From a152882c200eff7457ae7633302d505f83940306 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Wed, 26 Aug 2026 06:46:40 +0000 Subject: [PATCH 01/20] chore(deps): upgrade dependencies --- .github/workflows/pr.yml | 6 +- .github/workflows/release.yml | 319 +- Containerfile | 49 +- README.md | 8 +- bun.lock | 422 +- package.json | 33 +- src/bin/driver-process.ts | 4 +- src/contract/reducer.ts | 4 +- src/contract/state-validation.ts | 9 +- src/contract/state.ts | 3 +- src/core/agent-driver-backend.ts | 4 +- src/core/driver-command-dispatcher.ts | 15 +- src/core/driver-diagnostics.ts | 2 +- src/core/driver-permission-broker.ts | 24 +- src/core/driver-permission-policy.ts | 15 +- src/core/driver-runtime-io.ts | 29 +- src/host-ports/index.ts | 29 +- src/index.ts | 1 + .../runtime/driver-event-envelope.ts | 24 +- src/observability/driver-debug-runtime.ts | 23 +- src/projections/cma/outbound.ts | 16 + src/protocol/boot/testing.ts | 30 - src/protocol/events/index.ts | 6 +- src/protocol/events/runtime-event-types.ts | 4 +- .../events/runtime-event-validation.ts | 30 +- src/protocol/events/runtime-events.ts | 68 +- src/protocol/id/index.ts | 4 + src/protocol/orpc/index.ts | 48 +- src/runtime-command/index.ts | 61 +- .../acp/acp-assistant-transcript-state.ts | 292 +- .../acp/acp-client-request-handler.ts | 101 +- src/runtimes/acp/acp-configuration.ts | 21 +- src/runtimes/acp/acp-driver-backend.ts | 11 +- src/runtimes/acp/acp-event-translator.ts | 16 - src/runtimes/acp/acp-input-limit.ts | 42 +- src/runtimes/acp/acp-lifecycle-events.ts | 50 +- src/runtimes/acp/acp-permission-events.ts | 55 +- src/runtimes/acp/acp-session-setup.ts | 21 +- src/runtimes/acp/acp-session-update-events.ts | 87 +- src/runtimes/acp/acp-session-update-inbox.ts | 28 +- src/runtimes/acp/acp-terminal-manager.ts | 83 +- src/runtimes/acp/acp-tool-events.ts | 59 +- src/runtimes/acp/acp-turn-controller.ts | 151 +- src/runtimes/acp/acp-types.ts | 27 +- src/runtimes/acp/contract-item-projector.ts | 337 - src/runtimes/acp/contract-mapping.ts | 219 - .../acp/contract-permission-controller.ts | 412 -- .../acp/contract-session-update-inbox.ts | 156 - .../acp/contract-terminal-projector.ts | 378 - src/runtimes/acp/v1-contract-adapter.ts | 312 - .../claude/agent-sdk-driver-backend.ts | 101 +- src/runtimes/claude/agent-sdk-event-writer.ts | 496 +- src/runtimes/claude/agent-sdk-json.ts | 15 +- .../claude/agent-sdk-message-events.ts | 31 +- .../claude/agent-sdk-message-state.ts | 134 +- .../claude/agent-sdk-message-translator.ts | 538 +- src/runtimes/claude/agent-sdk-outcomes.ts | 183 + src/runtimes/claude/agent-sdk-prewarm.ts | 4 + .../claude/agent-sdk-query-options.ts | 19 +- src/runtimes/claude/agent-sdk-resume.ts | 16 +- src/runtimes/claude/agent-sdk-task-events.ts | 206 + src/runtimes/claude/agent-sdk-tool-id.ts | 35 + src/runtimes/claude/contract-adapter.ts | 153 - src/runtimes/claude/contract-items.ts | 137 - src/runtimes/claude/contract-permissions.ts | 526 -- src/runtimes/claude/contract-result.ts | 130 - .../claude/contract-transcript-state.ts | 161 - src/runtimes/claude/contract-transcript.ts | 763 -- src/runtimes/contract-adapter-meta.ts | 35 - src/runtimes/contract-projection-authority.ts | 181 - src/runtimes/contract-projection-preview.ts | 262 - src/runtimes/contract-projection-state.ts | 159 - src/runtimes/contract-projection.ts | 782 --- src/runtimes/driver-event-admission.ts | 71 +- src/runtimes/driver-event-publisher.ts | 579 +- src/runtimes/mcp/remote-http-mcp-executor.ts | 138 +- src/runtimes/openai/app-server-client.ts | 153 +- .../openai/app-server-driver-backend.ts | 304 +- .../openai/app-server-event-bridge.ts | 782 ++- .../openai/app-server-event-mapping.ts | 118 +- src/runtimes/openai/app-server-event-state.ts | 499 +- src/runtimes/openai/app-server-item-events.ts | 813 ++- src/runtimes/openai/app-server-json.ts | 14 + .../app-server-protocol-client-schemas.ts | 69 + .../openai/app-server-protocol-client.ts | 21 + .../openai/app-server-protocol-server.ts | 153 + .../openai/app-server-protocol-types.ts | 92 + .../{generated => }/app-server-protocol.ts | 0 .../openai/app-server-request-handler.ts | 88 +- .../openai/app-server-turn-tracker.ts | 54 +- .../openai/app-server-turn-validation.ts | 19 + src/runtimes/openai/contract-adapter-state.ts | 152 - src/runtimes/openai/contract-adapter-types.ts | 43 - src/runtimes/openai/contract-adapter.ts | 743 -- src/runtimes/openai/contract-interactions.ts | 393 -- src/runtimes/openai/contract-items.ts | 543 -- src/runtimes/openai/contract-turn-inbox.ts | 191 - .../openai/contract-turn-lifecycle.ts | 181 - src/runtimes/openai/event-translator.ts | 219 +- .../InitializeResponse.json | 33 + .../openai/generated-json-schema/README.md | 21 + .../ServerNotification.json | 6231 +++++++++++++++++ .../generated-json-schema/ServerRequest.json | 1605 +++++ ...hreadBackgroundTerminalsCleanResponse.json | 5 + .../ThreadInjectItemsResponse.json | 5 + .../ThreadResumeResponse.json | 2209 ++++++ .../ThreadStartResponse.json | 2169 ++++++ .../TurnStartResponse.json | 1514 ++++ .../openai/generated/AbsolutePathBuf.ts | 14 + .../generated/AgentMessageInputContent.ts | 7 + src/runtimes/openai/generated/AgentPath.ts | 5 + .../generated/ApplyPatchApprovalParams.ts | 24 + .../generated/ApplyPatchApprovalResponse.ts | 6 + src/runtimes/openai/generated/AuthMode.ts | 16 + .../generated/AutoCompactTokenLimitScope.ts | 9 + src/runtimes/openai/generated/ClientInfo.ts | 5 + .../openai/generated/ClientNotification.ts | 5 + .../openai/generated/ClientRequest.ts | 392 ++ .../generated/CodexResponseHandoffMode.ts | 5 + .../openai/generated/CollaborationMode.ts | 10 + src/runtimes/openai/generated/ContentItem.ts | 10 + .../openai/generated/ConversationGitInfo.ts | 9 + .../openai/generated/ConversationSummary.ts | 19 + .../openai/generated/ConversationTextRole.ts | 5 + .../generated/ExecCommandApprovalParams.ts | 22 + .../generated/ExecCommandApprovalResponse.ts | 6 + .../openai/generated/ExecPolicyAmendment.ts | 12 + src/runtimes/openai/generated/FileChange.ts | 8 + .../openai/generated/ForcedLoginMethod.ts | 5 + .../generated/FunctionCallOutputBody.ts | 6 + .../FunctionCallOutputContentItem.ts | 14 + .../generated/FuzzyFileSearchMatchType.ts | 5 + .../openai/generated/FuzzyFileSearchParams.ts | 9 + .../generated/FuzzyFileSearchResponse.ts | 6 + .../openai/generated/FuzzyFileSearchResult.ts | 16 + ...yFileSearchSessionCompletedNotification.ts | 5 + .../FuzzyFileSearchSessionStartParams.ts | 5 + .../FuzzyFileSearchSessionStartResponse.ts | 5 + .../FuzzyFileSearchSessionStopParams.ts | 5 + .../FuzzyFileSearchSessionStopResponse.ts | 5 + .../FuzzyFileSearchSessionUpdateParams.ts | 5 + .../FuzzyFileSearchSessionUpdateResponse.ts | 5 + ...zzyFileSearchSessionUpdatedNotification.ts | 10 + .../openai/generated/GetAuthStatusParams.ts | 5 + .../openai/generated/GetAuthStatusResponse.ts | 10 + .../generated/GetConversationSummaryParams.ts | 6 + .../GetConversationSummaryResponse.ts | 6 + .../openai/generated/GitDiffToRemoteParams.ts | 5 + .../generated/GitDiffToRemoteResponse.ts | 6 + src/runtimes/openai/generated/GitSha.ts | 5 + src/runtimes/openai/generated/ImageDetail.ts | 5 + .../generated/ImageGenerationFailure.ts | 9 + .../openai/generated/ImageGenerationItem.ts | 15 + .../generated/InitializeCapabilities.ts | 33 + .../openai/generated/InitializeParams.ts | 10 + .../openai/generated/InitializeResponse.ts | 22 + .../openai/generated/InputModality.ts | 8 + .../InternalChatMessageMetadataPassthrough.ts | 11 + .../openai/generated/InternalSessionSource.ts | 5 + .../openai/generated/LegacyAppPathString.ts | 27 + .../openai/generated/LocalShellAction.ts | 6 + .../openai/generated/LocalShellExecAction.ts | 11 + .../openai/generated/LocalShellStatus.ts | 5 + .../openai/generated/McpServerInfo.ts | 16 + src/runtimes/openai/generated/MessagePhase.ts | 11 + src/runtimes/openai/generated/ModeKind.ts | 8 + .../openai/generated/MultiAgentMode.ts | 9 + .../generated/NetworkPolicyAmendment.ts | 6 + .../generated/NetworkPolicyRuleAction.ts | 5 + .../openai/generated/ParsedCommand.ts | 20 + src/runtimes/openai/generated/PathUri.ts | 32 + src/runtimes/openai/generated/Personality.ts | 5 + src/runtimes/openai/generated/PlanType.ts | 22 + src/runtimes/openai/generated/README.md | 12 +- .../generated/RealtimeConversationVersion.ts | 5 + .../generated/RealtimeOutputModality.ts | 5 + .../openai/generated/RealtimeVoice.ts | 24 + .../openai/generated/RealtimeVoicesList.ts | 11 + .../openai/generated/ReasoningEffort.ts | 8 + .../openai/generated/ReasoningItemContent.ts | 7 + .../ReasoningItemReasoningSummary.ts | 5 + .../openai/generated/ReasoningSummary.ts | 10 + src/runtimes/openai/generated/RequestId.ts | 5 + src/runtimes/openai/generated/Resource.ts | 19 + .../openai/generated/ResourceContent.ts | 27 + .../openai/generated/ResourceTemplate.ts | 16 + src/runtimes/openai/generated/ResponseItem.ts | 138 + .../openai/generated/ResponseItemId.ts | 9 + .../openai/generated/ReviewDecision.ts | 18 + .../openai/generated/ServerNotification.ts | 193 + .../generated/ServerNotificationEnvelope.ts | 205 + .../openai/generated/ServerRequest.ts | 51 + .../openai/generated/SessionSource.ts | 15 + src/runtimes/openai/generated/Settings.ts | 13 + src/runtimes/openai/generated/SleepItem.ts | 8 + .../openai/generated/SubAgentSource.ts | 20 + src/runtimes/openai/generated/ThreadId.ts | 10 + .../openai/generated/ThreadMemoryMode.ts | 5 + src/runtimes/openai/generated/Tool.ts | 18 + src/runtimes/openai/generated/Verbosity.ts | 9 + .../openai/generated/WebSearchAction.ts | 9 + .../openai/generated/WebSearchContextSize.ts | 5 + .../openai/generated/WebSearchItem.ts | 18 + .../openai/generated/WebSearchLocation.ts | 10 + .../openai/generated/WebSearchMode.ts | 5 + .../openai/generated/WebSearchToolConfig.ts | 11 + .../generated/app-server-protocol-client.ts | 109 - .../generated/app-server-protocol-common.ts | 480 -- .../generated/app-server-protocol-server.ts | 368 - .../generated/app-server-protocol-types.ts | 466 -- src/runtimes/openai/generated/index.ts | 100 + .../openai/generated/serde_json/JsonValue.ts | 11 + src/runtimes/openai/generated/v2/Account.ts | 9 + .../v2/AccountLoginCompletedNotification.ts | 11 + .../AccountRateLimitsUpdatedNotification.ts | 13 + .../v2/AccountTokenUsageDailyBucket.ts | 5 + .../generated/v2/AccountTokenUsageSummary.ts | 11 + .../v2/AccountUpdatedNotification.ts | 7 + .../generated/v2/ActivePermissionProfile.ts | 16 + .../generated/v2/AddCreditsNudgeCreditType.ts | 5 + .../v2/AddCreditsNudgeEmailStatus.ts | 5 + .../generated/v2/AdditionalContextEntry.ts | 6 + .../generated/v2/AdditionalContextKind.ts | 5 + .../v2/AdditionalFileSystemPermissions.ts | 18 + .../v2/AdditionalNetworkPermissions.ts | 5 + .../v2/AdditionalPermissionProfile.ts | 13 + .../generated/v2/AgentMessageDelivery.ts | 5 + .../v2/AgentMessageDeltaNotification.ts | 10 + .../generated/v2/AllowDenyRequirement.ts | 5 + .../openai/generated/v2/AnalyticsConfig.ts | 14 + .../openai/generated/v2/AppBranding.ts | 15 + src/runtimes/openai/generated/v2/AppInfo.ts | 34 + .../v2/AppListUpdatedNotification.ts | 9 + .../openai/generated/v2/AppMetadata.ts | 19 + src/runtimes/openai/generated/v2/AppReview.ts | 5 + .../openai/generated/v2/AppScreenshot.ts | 5 + .../openai/generated/v2/AppSummary.ts | 14 + .../openai/generated/v2/AppTemplateSummary.ts | 16 + .../v2/AppTemplateUnavailableReason.ts | 5 + .../openai/generated/v2/AppToolApproval.ts | 5 + .../openai/generated/v2/AppToolSummary.ts | 15 + .../openai/generated/v2/AppToolsConfig.ts | 8 + .../openai/generated/v2/ApprovalsReviewer.ts | 12 + .../openai/generated/v2/AppsConfig.ts | 19 + .../openai/generated/v2/AppsDefaultConfig.ts | 13 + .../generated/v2/AppsInstalledParams.ts | 18 + .../generated/v2/AppsInstalledResponse.ts | 9 + .../openai/generated/v2/AppsListParams.ts | 25 + .../openai/generated/v2/AppsListResponse.ts | 16 + .../openai/generated/v2/AppsReadParams.ts | 22 + .../openai/generated/v2/AppsReadResponse.ts | 9 + .../openai/generated/v2/AskForApproval.ts | 17 + .../generated/v2/AttestationGenerateParams.ts | 5 + .../v2/AttestationGenerateResponse.ts | 10 + .../generated/v2/AutoReviewDecisionSource.ts | 8 + .../generated/v2/AutoReviewRequirements.ts | 8 + .../openai/generated/v2/AwsCredentialType.ts | 5 + .../openai/generated/v2/BedrockAwsProfile.ts | 5 + .../generated/v2/BedrockDiscoverParams.ts | 5 + .../generated/v2/BedrockDiscoverResponse.ts | 10 + .../v2/BedrockEnvironmentCredential.ts | 6 + .../openai/generated/v2/BedrockSetupParams.ts | 7 + .../generated/v2/BedrockSetupResponse.ts | 5 + .../v2/BrowserUseAccessApprovalLifetime.ts | 5 + .../openai/generated/v2/BrowserUseConfig.ts | 10 + .../generated/v2/BrowserUseOriginPolicy.ts | 15 + .../v2/BrowserUseOriginPolicyConfig.ts | 11 + .../generated/v2/BrowserUseRequirements.ts | 12 + src/runtimes/openai/generated/v2/ByteRange.ts | 5 + .../generated/v2/CancelLoginAccountParams.ts | 5 + .../v2/CancelLoginAccountResponse.ts | 6 + .../generated/v2/CancelLoginAccountStatus.ts | 5 + .../generated/v2/CapabilityRootLocation.ts | 15 + .../v2/ChatgptAuthTokensRefreshParams.ts | 18 + .../v2/ChatgptAuthTokensRefreshReason.ts | 5 + .../v2/ChatgptAuthTokensRefreshResponse.ts | 9 + .../v2/CliAuthCredentialsStoreMode.ts | 5 + .../openai/generated/v2/CodexErrorInfo.ts | 29 + .../openai/generated/v2/CollabAgentState.ts | 6 + .../openai/generated/v2/CollabAgentStatus.ts | 12 + .../openai/generated/v2/CollabAgentTool.ts | 14 + .../generated/v2/CollabAgentToolCallStatus.ts | 5 + .../v2/CollaborationModeListParams.ts | 8 + .../v2/CollaborationModeListResponse.ts | 9 + .../generated/v2/CollaborationModeMask.ts | 15 + .../openai/generated/v2/CommandAction.ts | 10 + .../v2/CommandExecOutputDeltaNotification.ts | 31 + .../generated/v2/CommandExecOutputStream.ts | 8 + .../openai/generated/v2/CommandExecParams.ts | 106 + .../generated/v2/CommandExecResizeParams.ts | 19 + .../generated/v2/CommandExecResizeResponse.ts | 8 + .../generated/v2/CommandExecResponse.ts | 25 + .../generated/v2/CommandExecTerminalSize.ts | 17 + .../v2/CommandExecTerminateParams.ts | 14 + .../v2/CommandExecTerminateResponse.ts | 8 + .../generated/v2/CommandExecWriteParams.ts | 23 + .../generated/v2/CommandExecWriteResponse.ts | 8 + .../v2/CommandExecutionApprovalDecision.ts | 13 + .../v2/CommandExecutionApprovalKind.ts | 8 + ...CommandExecutionOutputDeltaNotification.ts | 10 + .../CommandExecutionRequestApprovalParams.ts | 76 + ...CommandExecutionRequestApprovalResponse.ts | 8 + .../generated/v2/CommandExecutionSource.ts | 9 + .../generated/v2/CommandExecutionStatus.ts | 5 + .../openai/generated/v2/CommandMigration.ts | 5 + .../openai/generated/v2/ComputerUseConfig.ts | 12 + .../generated/v2/ComputerUseMacosConfig.ts | 8 + .../v2/ComputerUseMacosRequirements.ts | 8 + .../generated/v2/ComputerUseRequirements.ts | 14 + .../generated/v2/ComputerUseWindowsConfig.ts | 10 + .../v2/ComputerUseWindowsExeConfig.ts | 11 + .../v2/ComputerUseWindowsExeRequirement.ts | 11 + .../v2/ComputerUseWindowsRequirements.ts | 10 + src/runtimes/openai/generated/v2/Config.ts | 61 + .../generated/v2/ConfigBatchWriteParams.ts | 19 + .../openai/generated/v2/ConfigEdit.ts | 7 + .../openai/generated/v2/ConfigLayer.ts | 12 + .../generated/v2/ConfigLayerMetadata.ts | 6 + .../openai/generated/v2/ConfigLayerSource.ts | 52 + .../openai/generated/v2/ConfigReadParams.ts | 13 + .../openai/generated/v2/ConfigReadResponse.ts | 12 + .../openai/generated/v2/ConfigRequirements.ts | 52 + .../v2/ConfigRequirementsReadResponse.ts | 11 + .../generated/v2/ConfigValueWriteParams.ts | 16 + .../generated/v2/ConfigWarningNotification.ts | 23 + .../generated/v2/ConfigWriteResponse.ts | 16 + .../generated/v2/ConfiguredHookHandler.ts | 31 + .../v2/ConfiguredHookMatcherGroup.ts | 9 + .../openai/generated/v2/ConnectorMetadata.ts | 19 + ...nsumeAccountRateLimitResetCreditOutcome.ts | 9 + ...onsumeAccountRateLimitResetCreditParams.ts | 16 + ...sumeAccountRateLimitResetCreditResponse.ts | 8 + .../v2/ContextCompactedNotification.ts | 8 + .../openai/generated/v2/CreditsSnapshot.ts | 5 + .../generated/v2/CurrentTimeReadParams.ts | 5 + .../generated/v2/CurrentTimeReadResponse.ts | 10 + .../v2/DeprecationNoticeNotification.ts | 14 + .../v2/DesktopOnboardingEntrypoint.ts | 5 + .../v2/DynamicToolCallOutputContentItem.ts | 8 + .../generated/v2/DynamicToolCallParams.ts | 13 + .../generated/v2/DynamicToolCallResponse.ts | 9 + .../generated/v2/DynamicToolCallStatus.ts | 5 + .../generated/v2/DynamicToolFunctionSpec.ts | 11 + .../generated/v2/DynamicToolNamespaceSpec.ts | 10 + .../generated/v2/DynamicToolNamespaceTool.ts | 6 + .../openai/generated/v2/DynamicToolSpec.ts | 9 + .../generated/v2/EnvironmentAddParams.ts | 12 + .../generated/v2/EnvironmentAddResponse.ts | 5 + .../v2/EnvironmentConnectionNotification.ts | 5 + .../generated/v2/EnvironmentInfoParams.ts | 5 + .../generated/v2/EnvironmentInfoResponse.ts | 13 + .../generated/v2/EnvironmentShellInfo.ts | 14 + .../generated/v2/EnvironmentStatusKind.ts | 11 + .../generated/v2/EnvironmentStatusParams.ts | 13 + .../generated/v2/EnvironmentStatusResponse.ts | 18 + .../openai/generated/v2/ErrorNotification.ts | 11 + .../generated/v2/ExecPolicyAmendment.ts | 5 + .../generated/v2/ExperimentalFeature.ts | 38 + .../ExperimentalFeatureEnablementSetParams.ts | 13 + ...xperimentalFeatureEnablementSetResponse.ts | 10 + .../v2/ExperimentalFeatureListParams.ts | 20 + .../v2/ExperimentalFeatureListResponse.ts | 13 + .../generated/v2/ExperimentalFeatureStage.ts | 10 + .../v2/ExternalAgentConfigDetectParams.ts | 31 + .../v2/ExternalAgentConfigDetectResponse.ts | 10 + ...lAgentConfigImportCompletedNotification.ts | 9 + ...lAgentConfigImportHistoriesReadResponse.ts | 10 + .../v2/ExternalAgentConfigImportHistory.ts | 13 + ...nalAgentConfigImportHistoryRecordParams.ts | 15 + ...lAgentConfigImportHistoryRecordResponse.ts | 5 + ...tConfigImportHistoryRecordSuccessParams.ts | 15 + ...nfigImportHistoryRecordTypeResultParams.ts | 12 + ...xternalAgentConfigImportItemTypeFailure.ts | 14 + ...xternalAgentConfigImportItemTypeSuccess.ts | 15 + .../v2/ExternalAgentConfigImportParams.ts | 22 + ...alAgentConfigImportProgressNotification.ts | 9 + .../v2/ExternalAgentConfigImportResponse.ts | 5 + .../v2/ExternalAgentConfigImportTypeResult.ts | 12 + .../v2/ExternalAgentConfigMigrationItem.ts | 15 + .../ExternalAgentConfigMigrationItemType.ts | 15 + ...ExternalAgentDetectedConnectorCandidate.ts | 10 + .../ExternalAgentDetectedConnectorSource.ts | 5 + ...ExternalAgentImportedConnectorCandidate.ts | 10 + .../ExternalAgentImportedConnectorSource.ts | 5 + .../generated/v2/FeedbackRequirements.ts | 5 + .../generated/v2/FeedbackUploadParams.ts | 12 + .../generated/v2/FeedbackUploadResponse.ts | 5 + .../v2/FileChangeApprovalDecision.ts | 5 + .../v2/FileChangeOutputDeltaNotification.ts | 15 + .../v2/FileChangePatchUpdatedNotification.ts | 11 + .../v2/FileChangeRequestApprovalParams.ts | 22 + .../v2/FileChangeRequestApprovalResponse.ts | 6 + .../generated/v2/FileSystemAccessMode.ts | 5 + .../openai/generated/v2/FileSystemPath.ts | 10 + .../generated/v2/FileSystemSandboxEntry.ts | 7 + .../generated/v2/FileSystemSpecialPath.ts | 12 + .../openai/generated/v2/FileUpdateChange.ts | 6 + .../generated/v2/ForcedChatgptWorkspaceIds.ts | 8 + .../generated/v2/FsChangedNotification.ts | 18 + .../openai/generated/v2/FsCopyParams.ts | 22 + .../openai/generated/v2/FsCopyResponse.ts | 8 + .../generated/v2/FsCreateDirectoryParams.ts | 18 + .../generated/v2/FsCreateDirectoryResponse.ts | 8 + .../generated/v2/FsGetMetadataParams.ts | 14 + .../generated/v2/FsGetMetadataResponse.ts | 29 + .../generated/v2/FsReadDirectoryEntry.ts | 21 + .../generated/v2/FsReadDirectoryParams.ts | 14 + .../generated/v2/FsReadDirectoryResponse.ts | 14 + .../openai/generated/v2/FsReadFileParams.ts | 14 + .../openai/generated/v2/FsReadFileResponse.ts | 13 + .../openai/generated/v2/FsRemoveParams.ts | 22 + .../openai/generated/v2/FsRemoveResponse.ts | 8 + .../openai/generated/v2/FsUnwatchParams.ts | 13 + .../openai/generated/v2/FsUnwatchResponse.ts | 8 + .../openai/generated/v2/FsWatchParams.ts | 18 + .../openai/generated/v2/FsWatchResponse.ts | 14 + .../openai/generated/v2/FsWriteFileParams.ts | 18 + .../generated/v2/FsWriteFileResponse.ts | 8 + .../openai/generated/v2/GetAccountParams.ts | 14 + .../v2/GetAccountRateLimitsResponse.ts | 17 + .../openai/generated/v2/GetAccountResponse.ts | 6 + .../v2/GetAccountTokenUsageParams.ts | 10 + .../v2/GetAccountTokenUsageResponse.ts | 15 + .../v2/GetWorkspaceMessagesResponse.ts | 15 + src/runtimes/openai/generated/v2/GitInfo.ts | 5 + .../generated/v2/GrantedPermissionProfile.ts | 10 + .../generated/v2/GuardianApprovalReview.ts | 18 + .../v2/GuardianApprovalReviewAction.ts | 42 + .../v2/GuardianApprovalReviewStatus.ts | 13 + .../generated/v2/GuardianCommandSource.ts | 5 + .../openai/generated/v2/GuardianRiskLevel.ts | 8 + .../generated/v2/GuardianUserAuthorization.ts | 8 + .../v2/GuardianWarningNotification.ts | 14 + .../generated/v2/HookCompletedNotification.ts | 10 + .../openai/generated/v2/HookErrorInfo.ts | 5 + .../openai/generated/v2/HookEventName.ts | 17 + .../openai/generated/v2/HookExecutionMode.ts | 5 + .../openai/generated/v2/HookHandlerType.ts | 5 + .../openai/generated/v2/HookMetadata.ts | 33 + .../openai/generated/v2/HookMigration.ts | 5 + .../openai/generated/v2/HookOutputEntry.ts | 6 + .../generated/v2/HookOutputEntryKind.ts | 5 + .../openai/generated/v2/HookPromptFragment.ts | 5 + .../openai/generated/v2/HookRunStatus.ts | 5 + .../openai/generated/v2/HookRunSummary.ts | 28 + src/runtimes/openai/generated/v2/HookScope.ts | 5 + .../openai/generated/v2/HookSource.ts | 16 + .../generated/v2/HookStartedNotification.ts | 10 + .../openai/generated/v2/HookTrustStatus.ts | 5 + .../openai/generated/v2/HooksListEntry.ts | 12 + .../openai/generated/v2/HooksListParams.ts | 10 + .../openai/generated/v2/HooksListResponse.ts | 6 + .../generated/v2/InAppBrowserRequirements.ts | 5 + .../openai/generated/v2/InstalledApp.ts | 25 + .../generated/v2/ItemCompletedNotification.ts | 14 + ...dianApprovalReviewCompletedNotification.ts | 46 + ...ardianApprovalReviewStartedNotification.ts | 40 + .../generated/v2/ItemStartedNotification.ts | 14 + .../generated/v2/ListMcpServerStatusParams.ts | 21 + .../v2/ListMcpServerStatusResponse.ts | 13 + .../openai/generated/v2/LoginAccountParams.ts | 41 + .../generated/v2/LoginAccountResponse.ts | 28 + .../openai/generated/v2/LoginAppBrand.ts | 5 + .../generated/v2/LogoutAccountResponse.ts | 5 + .../generated/v2/ManagedHooksRequirements.ts | 21 + .../generated/v2/MarketplaceAddParams.ts | 9 + .../generated/v2/MarketplaceAddResponse.ts | 10 + .../generated/v2/MarketplaceInterface.ts | 5 + .../generated/v2/MarketplaceLoadErrorInfo.ts | 6 + .../generated/v2/MarketplaceRemoveParams.ts | 5 + .../generated/v2/MarketplaceRemoveResponse.ts | 9 + .../v2/MarketplaceUpgradeErrorInfo.ts | 5 + .../generated/v2/MarketplaceUpgradeParams.ts | 5 + .../v2/MarketplaceUpgradeResponse.ts | 11 + .../openai/generated/v2/McpAuthStatus.ts | 5 + .../generated/v2/McpElicitationArrayType.ts | 5 + .../v2/McpElicitationBooleanSchema.ts | 11 + .../generated/v2/McpElicitationBooleanType.ts | 5 + .../generated/v2/McpElicitationConstOption.ts | 5 + .../generated/v2/McpElicitationEnumSchema.ts | 11 + .../McpElicitationLegacyTitledEnumSchema.ts | 13 + .../v2/McpElicitationMultiSelectEnumSchema.ts | 9 + .../v2/McpElicitationNumberSchema.ts | 13 + .../generated/v2/McpElicitationNumberType.ts | 5 + .../generated/v2/McpElicitationObjectType.ts | 5 + .../v2/McpElicitationPrimitiveSchema.ts | 13 + .../generated/v2/McpElicitationSchema.ts | 18 + .../McpElicitationSingleSelectEnumSchema.ts | 9 + .../v2/McpElicitationStringFormat.ts | 5 + .../v2/McpElicitationStringSchema.ts | 15 + .../generated/v2/McpElicitationStringType.ts | 5 + .../v2/McpElicitationTitledEnumItems.ts | 6 + ...pElicitationTitledMultiSelectEnumSchema.ts | 15 + ...ElicitationTitledSingleSelectEnumSchema.ts | 13 + .../v2/McpElicitationUntitledEnumItems.ts | 9 + ...licitationUntitledMultiSelectEnumSchema.ts | 15 + ...icitationUntitledSingleSelectEnumSchema.ts | 12 + .../generated/v2/McpResourceReadParams.ts | 14 + .../generated/v2/McpResourceReadResponse.ts | 12 + .../generated/v2/McpServerConnectionStatus.ts | 12 + .../v2/McpServerElicitationAction.ts | 5 + .../v2/McpServerElicitationRequestParams.ts | 28 + .../v2/McpServerElicitationRequestResponse.ts | 19 + .../v2/McpServerEventNotification.ts | 6 + .../v2/McpServerEventStreamNotification.ts | 9 + .../v2/McpServerEventStreamStartParams.ts | 13 + .../v2/McpServerEventStreamStartResponse.ts | 5 + .../v2/McpServerEventStreamStopParams.ts | 5 + .../v2/McpServerEventStreamStopResponse.ts | 5 + .../openai/generated/v2/McpServerMigration.ts | 5 + .../v2/McpServerOauthClientRegistration.ts | 5 + ...cpServerOauthLoginCompletedNotification.ts | 10 + .../generated/v2/McpServerOauthLoginParams.ts | 15 + .../v2/McpServerOauthLoginResponse.ts | 5 + .../generated/v2/McpServerRefreshResponse.ts | 5 + .../v2/McpServerStartupFailureReason.ts | 5 + .../generated/v2/McpServerStartupState.ts | 5 + .../openai/generated/v2/McpServerStatus.ts | 23 + .../generated/v2/McpServerStatusDetail.ts | 5 + .../v2/McpServerStatusUpdatedNotification.ts | 13 + .../generated/v2/McpServerToolCallParams.ts | 12 + .../generated/v2/McpServerToolCallResponse.ts | 11 + .../generated/v2/McpToolCallAppContext.ts | 11 + .../openai/generated/v2/McpToolCallError.ts | 5 + .../v2/McpToolCallProgressNotification.ts | 10 + .../openai/generated/v2/McpToolCallResult.ts | 10 + .../openai/generated/v2/McpToolCallStatus.ts | 5 + .../openai/generated/v2/MemoryCitation.ts | 6 + .../generated/v2/MemoryCitationEntry.ts | 10 + .../generated/v2/MemoryResetResponse.ts | 5 + .../openai/generated/v2/MergeStrategy.ts | 5 + .../openai/generated/v2/MigrationDetails.ts | 21 + .../v2/MockExperimentalMethodParams.ts | 10 + .../v2/MockExperimentalMethodResponse.ts | 10 + src/runtimes/openai/generated/v2/Model.ts | 40 + .../generated/v2/ModelAvailabilityNux.ts | 5 + .../openai/generated/v2/ModelListParams.ts | 18 + .../openai/generated/v2/ModelListResponse.ts | 13 + .../v2/ModelProviderCapabilitiesReadParams.ts | 5 + .../ModelProviderCapabilitiesReadResponse.ts | 9 + .../openai/generated/v2/ModelRerouteReason.ts | 5 + .../generated/v2/ModelReroutedNotification.ts | 12 + ...ModelSafetyBufferingUpdatedNotification.ts | 13 + .../openai/generated/v2/ModelServiceTier.ts | 5 + .../openai/generated/v2/ModelUpgradeInfo.ts | 14 + .../openai/generated/v2/ModelVerification.ts | 5 + .../v2/ModelVerificationNotification.ts | 10 + .../openai/generated/v2/ModelsRequirements.ts | 6 + .../openai/generated/v2/MultiAgentVersion.ts | 8 + .../openai/generated/v2/NetworkAccess.ts | 5 + .../generated/v2/NetworkApprovalContext.ts | 6 + .../generated/v2/NetworkApprovalProtocol.ts | 5 + .../generated/v2/NetworkDomainPermission.ts | 5 + .../generated/v2/NetworkPolicyAmendment.ts | 6 + .../generated/v2/NetworkPolicyRuleAction.ts | 5 + .../generated/v2/NetworkRequirements.ts | 40 + .../v2/NetworkUnixSocketPermission.ts | 5 + .../generated/v2/NewThreadModelDefaults.ts | 10 + .../generated/v2/NonSteerableTurnKind.ts | 5 + .../openai/generated/v2/OverriddenMetadata.ts | 11 + .../openai/generated/v2/PatchApplyStatus.ts | 5 + .../openai/generated/v2/PatchChangeKind.ts | 8 + .../generated/v2/PermissionGrantScope.ts | 5 + .../v2/PermissionProfileListParams.ts | 18 + .../v2/PermissionProfileListResponse.ts | 13 + .../generated/v2/PermissionProfileSummary.ts | 18 + .../v2/PermissionsRequestApprovalParams.ts | 19 + .../v2/PermissionsRequestApprovalResponse.ts | 14 + .../generated/v2/PlanDeltaNotification.ts | 14 + .../openai/generated/v2/PluginAuthPolicy.ts | 5 + .../openai/generated/v2/PluginAvailability.ts | 5 + .../openai/generated/v2/PluginDetail.ts | 24 + .../generated/v2/PluginDisabledReason.ts | 9 + .../openai/generated/v2/PluginHookSummary.ts | 6 + .../generated/v2/PluginInstallParams.ts | 14 + .../generated/v2/PluginInstallPolicy.ts | 5 + .../generated/v2/PluginInstallPolicySource.ts | 5 + .../generated/v2/PluginInstallResponse.ts | 10 + .../generated/v2/PluginInstalledParams.ts | 16 + .../generated/v2/PluginInstalledResponse.ts | 10 + .../openai/generated/v2/PluginInterface.ts | 54 + .../generated/v2/PluginListMarketplaceKind.ts | 10 + .../openai/generated/v2/PluginListParams.ts | 22 + .../openai/generated/v2/PluginListResponse.ts | 11 + .../generated/v2/PluginMarketplaceEntry.ts | 17 + .../openai/generated/v2/PluginReadParams.ts | 10 + .../openai/generated/v2/PluginReadResponse.ts | 6 + .../openai/generated/v2/PluginSearchParams.ts | 13 + .../generated/v2/PluginSearchResponse.ts | 6 + .../openai/generated/v2/PluginSearchResult.ts | 11 + .../openai/generated/v2/PluginSearchScope.ts | 5 + .../generated/v2/PluginShareCheckoutParams.ts | 5 + .../v2/PluginShareCheckoutResponse.ts | 14 + .../openai/generated/v2/PluginShareContext.ts | 19 + .../generated/v2/PluginShareDeleteParams.ts | 5 + .../generated/v2/PluginShareDeleteResponse.ts | 5 + .../v2/PluginShareDiscoverability.ts | 5 + .../generated/v2/PluginShareListItem.ts | 10 + .../generated/v2/PluginShareListParams.ts | 5 + .../generated/v2/PluginShareListResponse.ts | 6 + .../generated/v2/PluginSharePrincipal.ts | 12 + .../generated/v2/PluginSharePrincipalRole.ts | 5 + .../generated/v2/PluginSharePrincipalType.ts | 5 + .../generated/v2/PluginShareSaveParams.ts | 13 + .../generated/v2/PluginShareSaveResponse.ts | 9 + .../openai/generated/v2/PluginShareTarget.ts | 11 + .../generated/v2/PluginShareTargetRole.ts | 5 + .../v2/PluginShareUpdateDiscoverability.ts | 5 + .../v2/PluginShareUpdateTargetsParams.ts | 11 + .../v2/PluginShareUpdateTargetsResponse.ts | 10 + .../generated/v2/PluginSkillReadParams.ts | 9 + .../generated/v2/PluginSkillReadResponse.ts | 5 + .../openai/generated/v2/PluginSource.ts | 21 + .../openai/generated/v2/PluginSummary.ts | 57 + .../generated/v2/PluginUninstallParams.ts | 5 + .../generated/v2/PluginUninstallResponse.ts | 5 + .../openai/generated/v2/PluginsMigration.ts | 5 + .../generated/v2/ProcessExitedNotification.ts | 43 + .../openai/generated/v2/ProcessKillParams.ts | 13 + .../generated/v2/ProcessKillResponse.ts | 8 + .../v2/ProcessOutputDeltaNotification.ts | 27 + .../generated/v2/ProcessOutputStream.ts | 8 + .../generated/v2/ProcessResizePtyParams.ts | 18 + .../generated/v2/ProcessResizePtyResponse.ts | 8 + .../openai/generated/v2/ProcessSpawnParams.ts | 74 + .../generated/v2/ProcessSpawnResponse.ts | 8 + .../generated/v2/ProcessTerminalSize.ts | 17 + .../generated/v2/ProcessWriteStdinParams.ts | 22 + .../generated/v2/ProcessWriteStdinResponse.ts | 8 + src/runtimes/openai/generated/v2/Project.ts | 14 + .../openai/generated/v2/ProjectChangeType.ts | 5 + .../v2/ProjectChangedNotification.ts | 6 + .../generated/v2/ProjectCreateParams.ts | 11 + .../generated/v2/ProjectCreateResponse.ts | 6 + .../generated/v2/ProjectDeleteParams.ts | 5 + .../generated/v2/ProjectDeleteResponse.ts | 5 + .../generated/v2/ProjectImportParams.ts | 12 + .../generated/v2/ProjectImportResponse.ts | 6 + .../openai/generated/v2/ProjectListParams.ts | 5 + .../generated/v2/ProjectListResponse.ts | 6 + .../openai/generated/v2/ProjectMoveParams.ts | 5 + .../generated/v2/ProjectMoveResponse.ts | 5 + .../openai/generated/v2/ProjectReadParams.ts | 5 + .../generated/v2/ProjectReadResponse.ts | 6 + .../openai/generated/v2/ProjectRoot.ts | 6 + .../generated/v2/ProjectUpdateParams.ts | 11 + .../generated/v2/ProjectUpdateResponse.ts | 6 + .../openai/generated/v2/QueuedSubmission.ts | 6 + .../generated/v2/RateLimitReachedType.ts | 10 + .../generated/v2/RateLimitResetCredit.ts | 30 + .../v2/RateLimitResetCreditStatus.ts | 5 + .../v2/RateLimitResetCreditsSummary.ts | 16 + .../openai/generated/v2/RateLimitResetType.ts | 5 + .../openai/generated/v2/RateLimitSnapshot.ts | 23 + .../openai/generated/v2/RateLimitWindow.ts | 9 + .../v2/RawResponseCompletedNotification.ts | 15 + .../RawResponseItemCompletedNotification.ts | 10 + .../generated/v2/ReasoningEffortOption.ts | 6 + .../ReasoningSummaryPartAddedNotification.ts | 10 + .../ReasoningSummaryTextDeltaNotification.ts | 11 + .../v2/ReasoningTextDeltaNotification.ts | 11 + .../generated/v2/RemoteControlClient.ts | 14 + .../v2/RemoteControlClientsListOrder.ts | 5 + .../v2/RemoteControlClientsListParams.ts | 11 + .../v2/RemoteControlClientsListResponse.ts | 9 + .../v2/RemoteControlClientsRevokeParams.ts | 5 + .../v2/RemoteControlClientsRevokeResponse.ts | 5 + .../v2/RemoteControlConnectionStatus.ts | 5 + .../v2/RemoteControlDisableParams.ts | 5 + .../v2/RemoteControlDisableResponse.ts | 11 + .../generated/v2/RemoteControlEnableParams.ts | 5 + .../v2/RemoteControlEnableResponse.ts | 11 + .../v2/RemoteControlPairingStartParams.ts | 5 + .../v2/RemoteControlPairingStartResponse.ts | 10 + .../v2/RemoteControlPairingStatusParams.ts | 8 + .../v2/RemoteControlPairingStatusResponse.ts | 5 + .../RemoteControlStatusChangedNotification.ts | 14 + .../v2/RemoteControlStatusReadResponse.ts | 11 + .../generated/v2/RequestPermissionProfile.ts | 10 + .../generated/v2/ResidencyRequirement.ts | 5 + .../openai/generated/v2/ReviewDelivery.ts | 5 + .../openai/generated/v2/ReviewStartParams.ts | 15 + .../generated/v2/ReviewStartResponse.ts | 15 + .../openai/generated/v2/ReviewTarget.ts | 16 + .../openai/generated/v2/SandboxMode.ts | 5 + .../openai/generated/v2/SandboxPolicy.ts | 17 + .../generated/v2/SandboxWorkspaceWrite.ts | 10 + .../generated/v2/ScheduledTaskSchedule.ts | 10 + .../generated/v2/ScheduledTaskSummary.ts | 11 + .../generated/v2/ScheduledTaskWeekday.ts | 5 + .../generated/v2/SelectedCapabilityRoot.ts | 18 + .../v2/SendAddCreditsNudgeEmailParams.ts | 6 + .../v2/SendAddCreditsNudgeEmailResponse.ts | 6 + .../generated/v2/ServerDiagnosticsGauge.ts | 5 + .../generated/v2/ServerDiagnosticsParams.ts | 5 + .../generated/v2/ServerDiagnosticsProcess.ts | 9 + .../generated/v2/ServerDiagnosticsResponse.ts | 10 + .../v2/ServerRequestResolvedNotification.ts | 6 + .../openai/generated/v2/SessionMigration.ts | 5 + .../openai/generated/v2/SessionSource.ts | 13 + .../openai/generated/v2/SkillDependencies.ts | 6 + .../openai/generated/v2/SkillErrorInfo.ts | 5 + .../openai/generated/v2/SkillInterface.ts | 21 + .../openai/generated/v2/SkillMetadata.ts | 25 + .../openai/generated/v2/SkillMigration.ts | 5 + .../openai/generated/v2/SkillScope.ts | 5 + .../openai/generated/v2/SkillSummary.ts | 14 + .../generated/v2/SkillToolDependency.ts | 12 + .../generated/v2/SkillsChangedNotification.ts | 11 + .../generated/v2/SkillsConfigWriteParams.ts | 16 + .../generated/v2/SkillsConfigWriteResponse.ts | 5 + .../generated/v2/SkillsExtraRootsSetParams.ts | 6 + .../v2/SkillsExtraRootsSetResponse.ts | 5 + .../openai/generated/v2/SkillsListEntry.ts | 11 + .../openai/generated/v2/SkillsListParams.ts | 14 + .../openai/generated/v2/SkillsListResponse.ts | 6 + .../openai/generated/v2/SortDirection.ts | 5 + .../generated/v2/SpendControlLimitSnapshot.ts | 10 + .../v2/StrictReviewRequiredNotification.ts | 12 + .../generated/v2/SubAgentActivityKind.ts | 5 + .../openai/generated/v2/SubagentMigration.ts | 5 + .../v2/TerminalInteractionNotification.ts | 11 + .../openai/generated/v2/TextElement.ts | 15 + .../openai/generated/v2/TextPosition.ts | 14 + src/runtimes/openai/generated/v2/TextRange.ts | 6 + src/runtimes/openai/generated/v2/Thread.ts | 127 + .../openai/generated/v2/ThreadActiveFlag.ts | 5 + ...ThreadApproveGuardianDeniedActionParams.ts | 12 + ...readApproveGuardianDeniedActionResponse.ts | 5 + .../generated/v2/ThreadArchiveParams.ts | 5 + .../generated/v2/ThreadArchiveResponse.ts | 5 + .../v2/ThreadArchivedNotification.ts | 5 + .../generated/v2/ThreadBackgroundTerminal.ts | 14 + .../ThreadBackgroundTerminalsCleanParams.ts | 5 + .../ThreadBackgroundTerminalsCleanResponse.ts | 5 + .../v2/ThreadBackgroundTerminalsListParams.ts | 15 + .../ThreadBackgroundTerminalsListResponse.ts | 13 + ...hreadBackgroundTerminalsTerminateParams.ts | 5 + ...eadBackgroundTerminalsTerminateResponse.ts | 5 + .../generated/v2/ThreadClosedNotification.ts | 5 + .../generated/v2/ThreadCompactStartParams.ts | 5 + .../v2/ThreadCompactStartResponse.ts | 5 + .../v2/ThreadDecrementElicitationParams.ts | 13 + .../v2/ThreadDecrementElicitationResponse.ts | 17 + .../openai/generated/v2/ThreadDeleteParams.ts | 5 + .../generated/v2/ThreadDeleteResponse.ts | 5 + .../generated/v2/ThreadDeletedNotification.ts | 5 + .../openai/generated/v2/ThreadExtra.ts | 8 + .../openai/generated/v2/ThreadForkParams.ts | 83 + .../openai/generated/v2/ThreadForkResponse.ts | 49 + .../openai/generated/v2/ThreadGoal.ts | 15 + .../generated/v2/ThreadGoalClearParams.ts | 5 + .../generated/v2/ThreadGoalClearResponse.ts | 5 + .../v2/ThreadGoalClearedNotification.ts | 5 + .../generated/v2/ThreadGoalGetParams.ts | 5 + .../generated/v2/ThreadGoalGetResponse.ts | 6 + .../generated/v2/ThreadGoalSetParams.ts | 11 + .../generated/v2/ThreadGoalSetResponse.ts | 6 + .../openai/generated/v2/ThreadGoalStatus.ts | 11 + .../v2/ThreadGoalUpdatedNotification.ts | 10 + .../openai/generated/v2/ThreadHistoryMode.ts | 5 + .../v2/ThreadIncrementElicitationParams.ts | 13 + .../v2/ThreadIncrementElicitationResponse.ts | 17 + .../generated/v2/ThreadInjectItemsParams.ts | 12 + .../generated/v2/ThreadInjectItemsResponse.ts | 5 + .../openai/generated/v2/ThreadItem.ts | 177 + .../openai/generated/v2/ThreadItemEntry.ts | 12 + .../generated/v2/ThreadItemsListParams.ts | 24 + .../generated/v2/ThreadItemsListResponse.ts | 18 + .../openai/generated/v2/ThreadListParams.ts | 74 + .../openai/generated/v2/ThreadListResponse.ts | 20 + .../generated/v2/ThreadLoadedListParams.ts | 14 + .../generated/v2/ThreadLoadedListResponse.ts | 15 + .../generated/v2/ThreadMemoryModeSetParams.ts | 6 + .../v2/ThreadMemoryModeSetResponse.ts | 5 + .../v2/ThreadMetadataGitInfoUpdateParams.ts | 21 + .../v2/ThreadMetadataUpdateParams.ts | 19 + .../v2/ThreadMetadataUpdateResponse.ts | 6 + .../v2/ThreadNameUpdatedNotification.ts | 5 + .../v2/ThreadProjectUpdatedNotification.ts | 5 + .../generated/v2/ThreadQueueAddParams.ts | 10 + .../generated/v2/ThreadQueueAddResponse.ts | 6 + .../v2/ThreadQueueChangedNotification.ts | 5 + .../generated/v2/ThreadQueueDeleteParams.ts | 5 + .../generated/v2/ThreadQueueDeleteResponse.ts | 5 + .../generated/v2/ThreadQueueListParams.ts | 15 + .../generated/v2/ThreadQueueListResponse.ts | 12 + .../generated/v2/ThreadQueueReorderParams.ts | 5 + .../v2/ThreadQueueReorderResponse.ts | 5 + .../generated/v2/ThreadQueueStartParams.ts | 5 + .../generated/v2/ThreadQueueStartResponse.ts | 6 + .../generated/v2/ThreadQueueUpdateParams.ts | 10 + .../generated/v2/ThreadQueueUpdateResponse.ts | 6 + .../openai/generated/v2/ThreadReadParams.ts | 11 + .../openai/generated/v2/ThreadReadResponse.ts | 6 + .../v2/ThreadRealtimeAppendAudioParams.ts | 9 + .../v2/ThreadRealtimeAppendAudioResponse.ts | 8 + .../v2/ThreadRealtimeAppendSpeechParams.ts | 8 + .../v2/ThreadRealtimeAppendSpeechResponse.ts | 8 + .../v2/ThreadRealtimeAppendTextParams.ts | 13 + .../v2/ThreadRealtimeAppendTextResponse.ts | 8 + .../generated/v2/ThreadRealtimeAudioChunk.ts | 14 + .../v2/ThreadRealtimeBemItemPresentation.ts | 11 + .../v2/ThreadRealtimeClosedNotification.ts | 8 + .../v2/ThreadRealtimeErrorNotification.ts | 8 + .../generated/v2/ThreadRealtimeInitialItem.ts | 9 + .../openai/generated/v2/ThreadRealtimeItem.ts | 21 + .../v2/ThreadRealtimeItemAddedNotification.ts | 9 + ...ThreadRealtimeItemCompletedNotification.ts | 12 + .../ThreadRealtimeItemStartedNotification.ts | 9 + ...RealtimeItemTranscriptDeltaNotification.ts | 12 + .../v2/ThreadRealtimeListVoicesParams.ts | 8 + .../v2/ThreadRealtimeListVoicesResponse.ts | 9 + ...eadRealtimeOutputAudioDeltaNotification.ts | 12 + .../v2/ThreadRealtimeSdpNotification.ts | 8 + .../v2/ThreadRealtimeSessionOutcome.ts | 5 + .../generated/v2/ThreadRealtimeStartParams.ts | 84 + .../v2/ThreadRealtimeStartResponse.ts | 8 + .../v2/ThreadRealtimeStartTransport.ts | 24 + .../v2/ThreadRealtimeStartedNotification.ts | 13 + .../generated/v2/ThreadRealtimeStopParams.ts | 8 + .../v2/ThreadRealtimeStopResponse.ts | 8 + ...readRealtimeTranscriptDeltaNotification.ts | 16 + ...hreadRealtimeTranscriptDoneNotification.ts | 16 + .../v2/ThreadRealtimeTranscriptRole.ts | 5 + .../v2/ThreadResumeInitialTurnsPageParams.ts | 20 + .../openai/generated/v2/ThreadResumeParams.ts | 82 + .../generated/v2/ThreadResumeResponse.ts | 68 + .../openai/generated/v2/ThreadRevertParams.ts | 16 + .../generated/v2/ThreadRevertResponse.ts | 26 + .../v2/ThreadRevertedNotification.ts | 5 + .../generated/v2/ThreadRollbackParams.ts | 17 + .../generated/v2/ThreadRollbackResponse.ts | 15 + .../generated/v2/ThreadSearchOccurrence.ts | 21 + .../v2/ThreadSearchOccurrencesParams.ts | 23 + .../v2/ThreadSearchOccurrencesResponse.ts | 15 + .../openai/generated/v2/ThreadSearchParams.ts | 39 + .../generated/v2/ThreadSearchResponse.ts | 20 + .../openai/generated/v2/ThreadSearchResult.ts | 6 + .../generated/v2/ThreadSearchSortKey.ts | 5 + .../generated/v2/ThreadSearchTextRange.ts | 17 + .../openai/generated/v2/ThreadSection.ts | 22 + .../generated/v2/ThreadSectionAppearance.ts | 8 + .../generated/v2/ThreadSectionCreateParams.ts | 15 + .../v2/ThreadSectionCreateResponse.ts | 9 + .../generated/v2/ThreadSectionDeleteParams.ts | 13 + .../v2/ThreadSectionDeleteResponse.ts | 8 + .../generated/v2/ThreadSectionListParams.ts | 17 + .../generated/v2/ThreadSectionListResponse.ts | 15 + .../generated/v2/ThreadSectionMoveParams.ts | 21 + .../generated/v2/ThreadSectionMoveResponse.ts | 5 + .../generated/v2/ThreadSectionUpdateParams.ts | 22 + .../v2/ThreadSectionUpdateResponse.ts | 9 + .../generated/v2/ThreadSetNameParams.ts | 5 + .../generated/v2/ThreadSetNameResponse.ts | 5 + .../openai/generated/v2/ThreadSettings.ts | 32 + .../v2/ThreadSettingsUpdateParams.ts | 68 + .../v2/ThreadSettingsUpdateResponse.ts | 5 + .../v2/ThreadSettingsUpdatedNotification.ts | 9 + .../generated/v2/ThreadShellCommandParams.ts | 14 + .../v2/ThreadShellCommandResponse.ts | 5 + .../openai/generated/v2/ThreadSortKey.ts | 5 + .../openai/generated/v2/ThreadSource.ts | 5 + .../openai/generated/v2/ThreadSourceKind.ts | 15 + .../openai/generated/v2/ThreadStartParams.ts | 91 + .../generated/v2/ThreadStartResponse.ts | 49 + .../openai/generated/v2/ThreadStartSource.ts | 5 + .../generated/v2/ThreadStartedNotification.ts | 6 + .../openai/generated/v2/ThreadStatus.ts | 10 + .../v2/ThreadStatusChangedNotification.ts | 6 + .../generated/v2/ThreadTimelineEntry.ts | 25 + .../generated/v2/ThreadTimelineListParams.ts | 12 + .../v2/ThreadTimelineListResponse.ts | 13 + .../openai/generated/v2/ThreadTokenUsage.ts | 10 + .../v2/ThreadTokenUsageUpdatedNotification.ts | 10 + .../generated/v2/ThreadTurnsListParams.ts | 25 + .../generated/v2/ThreadTurnsListResponse.ts | 20 + .../generated/v2/ThreadUnarchiveParams.ts | 5 + .../generated/v2/ThreadUnarchiveResponse.ts | 6 + .../v2/ThreadUnarchivedNotification.ts | 5 + .../generated/v2/ThreadUnsubscribeParams.ts | 5 + .../generated/v2/ThreadUnsubscribeResponse.ts | 6 + .../generated/v2/ThreadUnsubscribeStatus.ts | 5 + .../openai/generated/v2/ThreadUsage.ts | 11 + .../generated/v2/ThreadUsageBreakdownGroup.ts | 15 + .../generated/v2/TokenUsageBreakdown.ts | 12 + .../v2/ToolRequestUserInputAnswer.ts | 8 + .../v2/ToolRequestUserInputOption.ts | 8 + .../v2/ToolRequestUserInputParams.ts | 19 + .../v2/ToolRequestUserInputQuestion.ts | 16 + .../v2/ToolRequestUserInputResponse.ts | 11 + src/runtimes/openai/generated/v2/ToolsV2.ts | 6 + src/runtimes/openai/generated/v2/Turn.ts | 39 + .../generated/v2/TurnCompletedNotification.ts | 6 + .../v2/TurnDiffUpdatedNotification.ts | 9 + .../generated/v2/TurnEnvironmentParams.ts | 13 + src/runtimes/openai/generated/v2/TurnError.ts | 10 + .../generated/v2/TurnInterruptParams.ts | 5 + .../generated/v2/TurnInterruptResponse.ts | 5 + .../openai/generated/v2/TurnItemsView.ts | 5 + .../v2/TurnModerationMetadataNotification.ts | 10 + .../openai/generated/v2/TurnPlanStep.ts | 6 + .../openai/generated/v2/TurnPlanStepStatus.ts | 5 + .../v2/TurnPlanUpdatedNotification.ts | 11 + .../openai/generated/v2/TurnStartParams.ts | 108 + .../openai/generated/v2/TurnStartResponse.ts | 6 + .../generated/v2/TurnStartedNotification.ts | 6 + .../openai/generated/v2/TurnStatus.ts | 5 + .../openai/generated/v2/TurnSteerParams.ts | 30 + .../openai/generated/v2/TurnSteerResponse.ts | 5 + src/runtimes/openai/generated/v2/TurnsPage.ts | 10 + src/runtimes/openai/generated/v2/UserInput.ts | 21 + .../generated/v2/WarningNotification.ts | 14 + .../openai/generated/v2/WebSearchAction.ts | 9 + .../generated/v2/WindowsSandboxReadiness.ts | 5 + .../v2/WindowsSandboxReadinessResponse.ts | 6 + ...indowsSandboxSetupCompletedNotification.ts | 10 + .../generated/v2/WindowsSandboxSetupMode.ts | 5 + .../v2/WindowsSandboxSetupStartParams.ts | 10 + .../v2/WindowsSandboxSetupStartResponse.ts | 5 + ...WindowsWorldWritableWarningNotification.ts | 9 + .../openai/generated/v2/WorkspaceMessage.ts | 18 + .../generated/v2/WorkspaceMessageType.ts | 5 + .../openai/generated/v2/WriteStatus.ts | 5 + src/runtimes/openai/generated/v2/index.ts | 714 ++ .../openai/private-citation-filter.ts | 9 +- src/runtimes/provider-json.ts | 66 + src/runtimes/provider-registry.ts | 2 +- src/skill-package/archive.ts | 84 +- src/skill-package/path-admission.ts | 2 +- tests/acp-agent-process.test.ts | 5 +- tests/acp-client-request-handler.test.ts | 357 +- tests/acp-configuration.test.ts | 114 +- tests/acp-driver-backend.test.ts | 215 +- tests/acp-event-translator-lifecycle.test.ts | 484 +- ...cp-event-translator-session-update.test.ts | 268 +- tests/acp-file-system.test.ts | 1 + tests/acp-provider-fixtures.test.ts | 16 +- tests/acp-session-setup.test.ts | 70 +- tests/acp-terminal-manager.test.ts | 82 +- tests/acp-v1-contract-adapter-mapping.test.ts | 506 -- ...acp-v1-contract-adapter-permission.test.ts | 464 -- ...-v1-contract-adapter-run-lifecycle.test.ts | 514 -- .../acp-v1-contract-adapter-terminal.test.ts | 522 -- tests/agent-driver-kernel-commands.test.ts | 36 +- tests/architecture-boundaries.test.ts | 497 +- tests/claude-agent-sdk-driver-backend.test.ts | 143 +- tests/claude-agent-sdk-durability.test.ts | 741 ++ ...ent-sdk-provider-fixtures-terminal.test.ts | 380 +- ...t-sdk-provider-fixtures-transcript.test.ts | 722 +- tests/claude-agent-sdk-query-options.test.ts | 20 + tests/claude-agent-sdk-task-events.test.ts | 189 + tests/claude-agent-sdk-test-helpers.ts | 34 + ...claude-contract-adapter-permission.test.ts | 500 -- ...ude-contract-adapter-run-lifecycle.test.ts | 523 -- .../claude-contract-adapter-terminal.test.ts | 516 -- ...claude-contract-adapter-transcript.test.ts | 502 -- tests/cma-projection.test.ts | 162 + tests/contract-projection-authority.test.ts | 692 -- tests/contract-projection-preview.test.ts | 662 -- tests/contract-projection-state.test.ts | 845 --- tests/contract-reducer.test.ts | 8 +- tests/driver-artifact-contract.test.ts | 162 +- tests/driver-artifact-live.test.ts | 129 +- tests/driver-artifact-mcp.test.ts | 16 +- tests/driver-artifact-test-controller.ts | 80 +- .../driver-event-publisher-admission.test.ts | 1307 +++- ...river-event-publisher-backpressure.test.ts | 385 +- tests/driver-event-publisher-drain.test.ts | 2 +- tests/driver-golden-fixtures.test.ts | 60 +- tests/driver-permission-broker.test.ts | 92 + tests/driver-permission-policy.test.ts | 24 +- tests/driver-runtime-boundary-fixtures.ts | 15 +- tests/driver-runtime-boundary-state.test.ts | 14 + tests/driver-runtime-boundary-timing.test.ts | 87 + .../fixtures/architecture-boundaries-audit.ts | 22 + .../architecture-boundaries-resolution.ts | 25 + ...architecture-boundaries-runtime-loaders.ts | 26 + .../diagnostic-reported.json | 6 +- .../message-delta.json | 6 +- .../permission-requested.json | 6 +- .../runtime-event-envelopes/run-started.json | 6 +- .../tool-call-updated.json | 6 +- .../usage-updated.json | 6 +- .../providers/acp/cases/max-turn-failure.json | 31 +- .../acp/cases/permission-request.json | 36 +- .../providers/acp/cases/session-ready.json | 39 +- .../acp/cases/thought-and-unknown-update.json | 6 +- .../acp/cases/turn-text-tool-usage.json | 22 +- .../cases/result-failure-diagnostic.json | 47 +- .../stream-text-thinking-tool-result.json | 42 +- .../cases/system-files-and-session.json | 6 +- .../cases/unknown-message-diagnostic.json | 24 + .../cases/unknown-message-ignored.json | 10 - .../cases/agent-message-completed.json | 19 +- .../cases/command-output-stream.json | 16 +- .../cases/error-before-tracked-turn.json | 13 +- .../cases/reasoning-empty-summary.json | 1 + .../cases/root-with-parallel-child-turns.json | 63 +- ...rn-completed-with-final-agent-message.json | 19 +- .../cases/turn-plan-updated.json | 3 +- tests/fixtures/public-api-spaces-source.ts | 16 + tests/fixtures/public-api-type-namespace.ts | 10 + tests/fixtures/public-api-type-star-source.ts | 5 + tests/openai-app-server-client.test.ts | 456 +- .../openai-app-server-event-bridge-fixture.ts | 131 + ...enai-app-server-event-bridge-items.test.ts | 753 +- ...-server-event-bridge-run-lifecycle.test.ts | 320 +- ...i-app-server-event-bridge-terminal.test.ts | 1478 +++- ...app-server-event-bridge-transcript.test.ts | 184 +- ...penai-app-server-provider-fixtures.test.ts | 1190 +++- .../openai-app-server-request-handler.test.ts | 148 +- tests/openai-app-server-startup.test.ts | 725 +- tests/openai-app-server-turn-start.test.ts | 29 - ...enai-contract-adapter-interactions.test.ts | 662 -- tests/openai-contract-adapter-items.test.ts | 672 -- ...nai-contract-adapter-run-lifecycle.test.ts | 632 -- .../openai-contract-adapter-terminal.test.ts | 620 -- tests/opencode-acp-contract.test.ts | 10 +- tests/provider-registry.test.ts | 2 +- tests/public-api-exports.snapshot.json | 1 + tests/public-api-exports.test.ts | 162 +- tests/remote-http-mcp-executor.test.ts | 337 + tests/skill-materialization.test.ts | 25 +- 1024 files changed, 43912 insertions(+), 21248 deletions(-) delete mode 100644 src/protocol/boot/testing.ts delete mode 100644 src/runtimes/acp/acp-event-translator.ts delete mode 100644 src/runtimes/acp/contract-item-projector.ts delete mode 100644 src/runtimes/acp/contract-mapping.ts delete mode 100644 src/runtimes/acp/contract-permission-controller.ts delete mode 100644 src/runtimes/acp/contract-session-update-inbox.ts delete mode 100644 src/runtimes/acp/contract-terminal-projector.ts delete mode 100644 src/runtimes/acp/v1-contract-adapter.ts create mode 100644 src/runtimes/claude/agent-sdk-outcomes.ts create mode 100644 src/runtimes/claude/agent-sdk-task-events.ts create mode 100644 src/runtimes/claude/agent-sdk-tool-id.ts delete mode 100644 src/runtimes/claude/contract-adapter.ts delete mode 100644 src/runtimes/claude/contract-items.ts delete mode 100644 src/runtimes/claude/contract-permissions.ts delete mode 100644 src/runtimes/claude/contract-result.ts delete mode 100644 src/runtimes/claude/contract-transcript-state.ts delete mode 100644 src/runtimes/claude/contract-transcript.ts delete mode 100644 src/runtimes/contract-adapter-meta.ts delete mode 100644 src/runtimes/contract-projection-authority.ts delete mode 100644 src/runtimes/contract-projection-preview.ts delete mode 100644 src/runtimes/contract-projection-state.ts delete mode 100644 src/runtimes/contract-projection.ts create mode 100644 src/runtimes/openai/app-server-protocol-client-schemas.ts create mode 100644 src/runtimes/openai/app-server-protocol-client.ts create mode 100644 src/runtimes/openai/app-server-protocol-server.ts create mode 100644 src/runtimes/openai/app-server-protocol-types.ts rename src/runtimes/openai/{generated => }/app-server-protocol.ts (100%) create mode 100644 src/runtimes/openai/app-server-turn-validation.ts delete mode 100644 src/runtimes/openai/contract-adapter-state.ts delete mode 100644 src/runtimes/openai/contract-adapter-types.ts delete mode 100644 src/runtimes/openai/contract-adapter.ts delete mode 100644 src/runtimes/openai/contract-interactions.ts delete mode 100644 src/runtimes/openai/contract-items.ts delete mode 100644 src/runtimes/openai/contract-turn-inbox.ts delete mode 100644 src/runtimes/openai/contract-turn-lifecycle.ts create mode 100644 src/runtimes/openai/generated-json-schema/InitializeResponse.json create mode 100644 src/runtimes/openai/generated-json-schema/README.md create mode 100644 src/runtimes/openai/generated-json-schema/ServerNotification.json create mode 100644 src/runtimes/openai/generated-json-schema/ServerRequest.json create mode 100644 src/runtimes/openai/generated-json-schema/ThreadBackgroundTerminalsCleanResponse.json create mode 100644 src/runtimes/openai/generated-json-schema/ThreadInjectItemsResponse.json create mode 100644 src/runtimes/openai/generated-json-schema/ThreadResumeResponse.json create mode 100644 src/runtimes/openai/generated-json-schema/ThreadStartResponse.json create mode 100644 src/runtimes/openai/generated-json-schema/TurnStartResponse.json create mode 100644 src/runtimes/openai/generated/AbsolutePathBuf.ts create mode 100644 src/runtimes/openai/generated/AgentMessageInputContent.ts create mode 100644 src/runtimes/openai/generated/AgentPath.ts create mode 100644 src/runtimes/openai/generated/ApplyPatchApprovalParams.ts create mode 100644 src/runtimes/openai/generated/ApplyPatchApprovalResponse.ts create mode 100644 src/runtimes/openai/generated/AuthMode.ts create mode 100644 src/runtimes/openai/generated/AutoCompactTokenLimitScope.ts create mode 100644 src/runtimes/openai/generated/ClientInfo.ts create mode 100644 src/runtimes/openai/generated/ClientNotification.ts create mode 100644 src/runtimes/openai/generated/ClientRequest.ts create mode 100644 src/runtimes/openai/generated/CodexResponseHandoffMode.ts create mode 100644 src/runtimes/openai/generated/CollaborationMode.ts create mode 100644 src/runtimes/openai/generated/ContentItem.ts create mode 100644 src/runtimes/openai/generated/ConversationGitInfo.ts create mode 100644 src/runtimes/openai/generated/ConversationSummary.ts create mode 100644 src/runtimes/openai/generated/ConversationTextRole.ts create mode 100644 src/runtimes/openai/generated/ExecCommandApprovalParams.ts create mode 100644 src/runtimes/openai/generated/ExecCommandApprovalResponse.ts create mode 100644 src/runtimes/openai/generated/ExecPolicyAmendment.ts create mode 100644 src/runtimes/openai/generated/FileChange.ts create mode 100644 src/runtimes/openai/generated/ForcedLoginMethod.ts create mode 100644 src/runtimes/openai/generated/FunctionCallOutputBody.ts create mode 100644 src/runtimes/openai/generated/FunctionCallOutputContentItem.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchMatchType.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchParams.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchResponse.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchResult.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionStartParams.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionStartResponse.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionStopParams.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionStopResponse.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateParams.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateResponse.ts create mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/GetAuthStatusParams.ts create mode 100644 src/runtimes/openai/generated/GetAuthStatusResponse.ts create mode 100644 src/runtimes/openai/generated/GetConversationSummaryParams.ts create mode 100644 src/runtimes/openai/generated/GetConversationSummaryResponse.ts create mode 100644 src/runtimes/openai/generated/GitDiffToRemoteParams.ts create mode 100644 src/runtimes/openai/generated/GitDiffToRemoteResponse.ts create mode 100644 src/runtimes/openai/generated/GitSha.ts create mode 100644 src/runtimes/openai/generated/ImageDetail.ts create mode 100644 src/runtimes/openai/generated/ImageGenerationFailure.ts create mode 100644 src/runtimes/openai/generated/ImageGenerationItem.ts create mode 100644 src/runtimes/openai/generated/InitializeCapabilities.ts create mode 100644 src/runtimes/openai/generated/InitializeParams.ts create mode 100644 src/runtimes/openai/generated/InitializeResponse.ts create mode 100644 src/runtimes/openai/generated/InputModality.ts create mode 100644 src/runtimes/openai/generated/InternalChatMessageMetadataPassthrough.ts create mode 100644 src/runtimes/openai/generated/InternalSessionSource.ts create mode 100644 src/runtimes/openai/generated/LegacyAppPathString.ts create mode 100644 src/runtimes/openai/generated/LocalShellAction.ts create mode 100644 src/runtimes/openai/generated/LocalShellExecAction.ts create mode 100644 src/runtimes/openai/generated/LocalShellStatus.ts create mode 100644 src/runtimes/openai/generated/McpServerInfo.ts create mode 100644 src/runtimes/openai/generated/MessagePhase.ts create mode 100644 src/runtimes/openai/generated/ModeKind.ts create mode 100644 src/runtimes/openai/generated/MultiAgentMode.ts create mode 100644 src/runtimes/openai/generated/NetworkPolicyAmendment.ts create mode 100644 src/runtimes/openai/generated/NetworkPolicyRuleAction.ts create mode 100644 src/runtimes/openai/generated/ParsedCommand.ts create mode 100644 src/runtimes/openai/generated/PathUri.ts create mode 100644 src/runtimes/openai/generated/Personality.ts create mode 100644 src/runtimes/openai/generated/PlanType.ts create mode 100644 src/runtimes/openai/generated/RealtimeConversationVersion.ts create mode 100644 src/runtimes/openai/generated/RealtimeOutputModality.ts create mode 100644 src/runtimes/openai/generated/RealtimeVoice.ts create mode 100644 src/runtimes/openai/generated/RealtimeVoicesList.ts create mode 100644 src/runtimes/openai/generated/ReasoningEffort.ts create mode 100644 src/runtimes/openai/generated/ReasoningItemContent.ts create mode 100644 src/runtimes/openai/generated/ReasoningItemReasoningSummary.ts create mode 100644 src/runtimes/openai/generated/ReasoningSummary.ts create mode 100644 src/runtimes/openai/generated/RequestId.ts create mode 100644 src/runtimes/openai/generated/Resource.ts create mode 100644 src/runtimes/openai/generated/ResourceContent.ts create mode 100644 src/runtimes/openai/generated/ResourceTemplate.ts create mode 100644 src/runtimes/openai/generated/ResponseItem.ts create mode 100644 src/runtimes/openai/generated/ResponseItemId.ts create mode 100644 src/runtimes/openai/generated/ReviewDecision.ts create mode 100644 src/runtimes/openai/generated/ServerNotification.ts create mode 100644 src/runtimes/openai/generated/ServerNotificationEnvelope.ts create mode 100644 src/runtimes/openai/generated/ServerRequest.ts create mode 100644 src/runtimes/openai/generated/SessionSource.ts create mode 100644 src/runtimes/openai/generated/Settings.ts create mode 100644 src/runtimes/openai/generated/SleepItem.ts create mode 100644 src/runtimes/openai/generated/SubAgentSource.ts create mode 100644 src/runtimes/openai/generated/ThreadId.ts create mode 100644 src/runtimes/openai/generated/ThreadMemoryMode.ts create mode 100644 src/runtimes/openai/generated/Tool.ts create mode 100644 src/runtimes/openai/generated/Verbosity.ts create mode 100644 src/runtimes/openai/generated/WebSearchAction.ts create mode 100644 src/runtimes/openai/generated/WebSearchContextSize.ts create mode 100644 src/runtimes/openai/generated/WebSearchItem.ts create mode 100644 src/runtimes/openai/generated/WebSearchLocation.ts create mode 100644 src/runtimes/openai/generated/WebSearchMode.ts create mode 100644 src/runtimes/openai/generated/WebSearchToolConfig.ts delete mode 100644 src/runtimes/openai/generated/app-server-protocol-client.ts delete mode 100644 src/runtimes/openai/generated/app-server-protocol-common.ts delete mode 100644 src/runtimes/openai/generated/app-server-protocol-server.ts delete mode 100644 src/runtimes/openai/generated/app-server-protocol-types.ts create mode 100644 src/runtimes/openai/generated/index.ts create mode 100644 src/runtimes/openai/generated/serde_json/JsonValue.ts create mode 100644 src/runtimes/openai/generated/v2/Account.ts create mode 100644 src/runtimes/openai/generated/v2/AccountLoginCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/AccountRateLimitsUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/AccountTokenUsageDailyBucket.ts create mode 100644 src/runtimes/openai/generated/v2/AccountTokenUsageSummary.ts create mode 100644 src/runtimes/openai/generated/v2/AccountUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ActivePermissionProfile.ts create mode 100644 src/runtimes/openai/generated/v2/AddCreditsNudgeCreditType.ts create mode 100644 src/runtimes/openai/generated/v2/AddCreditsNudgeEmailStatus.ts create mode 100644 src/runtimes/openai/generated/v2/AdditionalContextEntry.ts create mode 100644 src/runtimes/openai/generated/v2/AdditionalContextKind.ts create mode 100644 src/runtimes/openai/generated/v2/AdditionalFileSystemPermissions.ts create mode 100644 src/runtimes/openai/generated/v2/AdditionalNetworkPermissions.ts create mode 100644 src/runtimes/openai/generated/v2/AdditionalPermissionProfile.ts create mode 100644 src/runtimes/openai/generated/v2/AgentMessageDelivery.ts create mode 100644 src/runtimes/openai/generated/v2/AgentMessageDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/AllowDenyRequirement.ts create mode 100644 src/runtimes/openai/generated/v2/AnalyticsConfig.ts create mode 100644 src/runtimes/openai/generated/v2/AppBranding.ts create mode 100644 src/runtimes/openai/generated/v2/AppInfo.ts create mode 100644 src/runtimes/openai/generated/v2/AppListUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/AppMetadata.ts create mode 100644 src/runtimes/openai/generated/v2/AppReview.ts create mode 100644 src/runtimes/openai/generated/v2/AppScreenshot.ts create mode 100644 src/runtimes/openai/generated/v2/AppSummary.ts create mode 100644 src/runtimes/openai/generated/v2/AppTemplateSummary.ts create mode 100644 src/runtimes/openai/generated/v2/AppTemplateUnavailableReason.ts create mode 100644 src/runtimes/openai/generated/v2/AppToolApproval.ts create mode 100644 src/runtimes/openai/generated/v2/AppToolSummary.ts create mode 100644 src/runtimes/openai/generated/v2/AppToolsConfig.ts create mode 100644 src/runtimes/openai/generated/v2/ApprovalsReviewer.ts create mode 100644 src/runtimes/openai/generated/v2/AppsConfig.ts create mode 100644 src/runtimes/openai/generated/v2/AppsDefaultConfig.ts create mode 100644 src/runtimes/openai/generated/v2/AppsInstalledParams.ts create mode 100644 src/runtimes/openai/generated/v2/AppsInstalledResponse.ts create mode 100644 src/runtimes/openai/generated/v2/AppsListParams.ts create mode 100644 src/runtimes/openai/generated/v2/AppsListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/AppsReadParams.ts create mode 100644 src/runtimes/openai/generated/v2/AppsReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/AskForApproval.ts create mode 100644 src/runtimes/openai/generated/v2/AttestationGenerateParams.ts create mode 100644 src/runtimes/openai/generated/v2/AttestationGenerateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/AutoReviewDecisionSource.ts create mode 100644 src/runtimes/openai/generated/v2/AutoReviewRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/AwsCredentialType.ts create mode 100644 src/runtimes/openai/generated/v2/BedrockAwsProfile.ts create mode 100644 src/runtimes/openai/generated/v2/BedrockDiscoverParams.ts create mode 100644 src/runtimes/openai/generated/v2/BedrockDiscoverResponse.ts create mode 100644 src/runtimes/openai/generated/v2/BedrockEnvironmentCredential.ts create mode 100644 src/runtimes/openai/generated/v2/BedrockSetupParams.ts create mode 100644 src/runtimes/openai/generated/v2/BedrockSetupResponse.ts create mode 100644 src/runtimes/openai/generated/v2/BrowserUseAccessApprovalLifetime.ts create mode 100644 src/runtimes/openai/generated/v2/BrowserUseConfig.ts create mode 100644 src/runtimes/openai/generated/v2/BrowserUseOriginPolicy.ts create mode 100644 src/runtimes/openai/generated/v2/BrowserUseOriginPolicyConfig.ts create mode 100644 src/runtimes/openai/generated/v2/BrowserUseRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/ByteRange.ts create mode 100644 src/runtimes/openai/generated/v2/CancelLoginAccountParams.ts create mode 100644 src/runtimes/openai/generated/v2/CancelLoginAccountResponse.ts create mode 100644 src/runtimes/openai/generated/v2/CancelLoginAccountStatus.ts create mode 100644 src/runtimes/openai/generated/v2/CapabilityRootLocation.ts create mode 100644 src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshParams.ts create mode 100644 src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshReason.ts create mode 100644 src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshResponse.ts create mode 100644 src/runtimes/openai/generated/v2/CliAuthCredentialsStoreMode.ts create mode 100644 src/runtimes/openai/generated/v2/CodexErrorInfo.ts create mode 100644 src/runtimes/openai/generated/v2/CollabAgentState.ts create mode 100644 src/runtimes/openai/generated/v2/CollabAgentStatus.ts create mode 100644 src/runtimes/openai/generated/v2/CollabAgentTool.ts create mode 100644 src/runtimes/openai/generated/v2/CollabAgentToolCallStatus.ts create mode 100644 src/runtimes/openai/generated/v2/CollaborationModeListParams.ts create mode 100644 src/runtimes/openai/generated/v2/CollaborationModeListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/CollaborationModeMask.ts create mode 100644 src/runtimes/openai/generated/v2/CommandAction.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecOutputDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecOutputStream.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecParams.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecResizeParams.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecResizeResponse.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecResponse.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecTerminalSize.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecTerminateParams.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecTerminateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecWriteParams.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecWriteResponse.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecutionApprovalDecision.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecutionApprovalKind.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecutionOutputDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalParams.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalResponse.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecutionSource.ts create mode 100644 src/runtimes/openai/generated/v2/CommandExecutionStatus.ts create mode 100644 src/runtimes/openai/generated/v2/CommandMigration.ts create mode 100644 src/runtimes/openai/generated/v2/ComputerUseConfig.ts create mode 100644 src/runtimes/openai/generated/v2/ComputerUseMacosConfig.ts create mode 100644 src/runtimes/openai/generated/v2/ComputerUseMacosRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/ComputerUseRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/ComputerUseWindowsConfig.ts create mode 100644 src/runtimes/openai/generated/v2/ComputerUseWindowsExeConfig.ts create mode 100644 src/runtimes/openai/generated/v2/ComputerUseWindowsExeRequirement.ts create mode 100644 src/runtimes/openai/generated/v2/ComputerUseWindowsRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/Config.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigBatchWriteParams.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigEdit.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigLayer.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigLayerMetadata.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigLayerSource.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigReadParams.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigRequirementsReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigValueWriteParams.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigWarningNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ConfigWriteResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ConfiguredHookHandler.ts create mode 100644 src/runtimes/openai/generated/v2/ConfiguredHookMatcherGroup.ts create mode 100644 src/runtimes/openai/generated/v2/ConnectorMetadata.ts create mode 100644 src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditOutcome.ts create mode 100644 src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditParams.ts create mode 100644 src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ContextCompactedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/CreditsSnapshot.ts create mode 100644 src/runtimes/openai/generated/v2/CurrentTimeReadParams.ts create mode 100644 src/runtimes/openai/generated/v2/CurrentTimeReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/DeprecationNoticeNotification.ts create mode 100644 src/runtimes/openai/generated/v2/DesktopOnboardingEntrypoint.ts create mode 100644 src/runtimes/openai/generated/v2/DynamicToolCallOutputContentItem.ts create mode 100644 src/runtimes/openai/generated/v2/DynamicToolCallParams.ts create mode 100644 src/runtimes/openai/generated/v2/DynamicToolCallResponse.ts create mode 100644 src/runtimes/openai/generated/v2/DynamicToolCallStatus.ts create mode 100644 src/runtimes/openai/generated/v2/DynamicToolFunctionSpec.ts create mode 100644 src/runtimes/openai/generated/v2/DynamicToolNamespaceSpec.ts create mode 100644 src/runtimes/openai/generated/v2/DynamicToolNamespaceTool.ts create mode 100644 src/runtimes/openai/generated/v2/DynamicToolSpec.ts create mode 100644 src/runtimes/openai/generated/v2/EnvironmentAddParams.ts create mode 100644 src/runtimes/openai/generated/v2/EnvironmentAddResponse.ts create mode 100644 src/runtimes/openai/generated/v2/EnvironmentConnectionNotification.ts create mode 100644 src/runtimes/openai/generated/v2/EnvironmentInfoParams.ts create mode 100644 src/runtimes/openai/generated/v2/EnvironmentInfoResponse.ts create mode 100644 src/runtimes/openai/generated/v2/EnvironmentShellInfo.ts create mode 100644 src/runtimes/openai/generated/v2/EnvironmentStatusKind.ts create mode 100644 src/runtimes/openai/generated/v2/EnvironmentStatusParams.ts create mode 100644 src/runtimes/openai/generated/v2/EnvironmentStatusResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ErrorNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ExecPolicyAmendment.ts create mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeature.ts create mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetParams.ts create mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureStage.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigDetectParams.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigDetectResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistory.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordParams.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordSuccessParams.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordTypeResultParams.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeFailure.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeSuccess.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportParams.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportProgressNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportTypeResult.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItem.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItemType.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorCandidate.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorSource.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorCandidate.ts create mode 100644 src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorSource.ts create mode 100644 src/runtimes/openai/generated/v2/FeedbackRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/FeedbackUploadParams.ts create mode 100644 src/runtimes/openai/generated/v2/FeedbackUploadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FileChangeApprovalDecision.ts create mode 100644 src/runtimes/openai/generated/v2/FileChangeOutputDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/FileChangePatchUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/FileChangeRequestApprovalParams.ts create mode 100644 src/runtimes/openai/generated/v2/FileChangeRequestApprovalResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FileSystemAccessMode.ts create mode 100644 src/runtimes/openai/generated/v2/FileSystemPath.ts create mode 100644 src/runtimes/openai/generated/v2/FileSystemSandboxEntry.ts create mode 100644 src/runtimes/openai/generated/v2/FileSystemSpecialPath.ts create mode 100644 src/runtimes/openai/generated/v2/FileUpdateChange.ts create mode 100644 src/runtimes/openai/generated/v2/ForcedChatgptWorkspaceIds.ts create mode 100644 src/runtimes/openai/generated/v2/FsChangedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/FsCopyParams.ts create mode 100644 src/runtimes/openai/generated/v2/FsCopyResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FsCreateDirectoryParams.ts create mode 100644 src/runtimes/openai/generated/v2/FsCreateDirectoryResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FsGetMetadataParams.ts create mode 100644 src/runtimes/openai/generated/v2/FsGetMetadataResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FsReadDirectoryEntry.ts create mode 100644 src/runtimes/openai/generated/v2/FsReadDirectoryParams.ts create mode 100644 src/runtimes/openai/generated/v2/FsReadDirectoryResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FsReadFileParams.ts create mode 100644 src/runtimes/openai/generated/v2/FsReadFileResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FsRemoveParams.ts create mode 100644 src/runtimes/openai/generated/v2/FsRemoveResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FsUnwatchParams.ts create mode 100644 src/runtimes/openai/generated/v2/FsUnwatchResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FsWatchParams.ts create mode 100644 src/runtimes/openai/generated/v2/FsWatchResponse.ts create mode 100644 src/runtimes/openai/generated/v2/FsWriteFileParams.ts create mode 100644 src/runtimes/openai/generated/v2/FsWriteFileResponse.ts create mode 100644 src/runtimes/openai/generated/v2/GetAccountParams.ts create mode 100644 src/runtimes/openai/generated/v2/GetAccountRateLimitsResponse.ts create mode 100644 src/runtimes/openai/generated/v2/GetAccountResponse.ts create mode 100644 src/runtimes/openai/generated/v2/GetAccountTokenUsageParams.ts create mode 100644 src/runtimes/openai/generated/v2/GetAccountTokenUsageResponse.ts create mode 100644 src/runtimes/openai/generated/v2/GetWorkspaceMessagesResponse.ts create mode 100644 src/runtimes/openai/generated/v2/GitInfo.ts create mode 100644 src/runtimes/openai/generated/v2/GrantedPermissionProfile.ts create mode 100644 src/runtimes/openai/generated/v2/GuardianApprovalReview.ts create mode 100644 src/runtimes/openai/generated/v2/GuardianApprovalReviewAction.ts create mode 100644 src/runtimes/openai/generated/v2/GuardianApprovalReviewStatus.ts create mode 100644 src/runtimes/openai/generated/v2/GuardianCommandSource.ts create mode 100644 src/runtimes/openai/generated/v2/GuardianRiskLevel.ts create mode 100644 src/runtimes/openai/generated/v2/GuardianUserAuthorization.ts create mode 100644 src/runtimes/openai/generated/v2/GuardianWarningNotification.ts create mode 100644 src/runtimes/openai/generated/v2/HookCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/HookErrorInfo.ts create mode 100644 src/runtimes/openai/generated/v2/HookEventName.ts create mode 100644 src/runtimes/openai/generated/v2/HookExecutionMode.ts create mode 100644 src/runtimes/openai/generated/v2/HookHandlerType.ts create mode 100644 src/runtimes/openai/generated/v2/HookMetadata.ts create mode 100644 src/runtimes/openai/generated/v2/HookMigration.ts create mode 100644 src/runtimes/openai/generated/v2/HookOutputEntry.ts create mode 100644 src/runtimes/openai/generated/v2/HookOutputEntryKind.ts create mode 100644 src/runtimes/openai/generated/v2/HookPromptFragment.ts create mode 100644 src/runtimes/openai/generated/v2/HookRunStatus.ts create mode 100644 src/runtimes/openai/generated/v2/HookRunSummary.ts create mode 100644 src/runtimes/openai/generated/v2/HookScope.ts create mode 100644 src/runtimes/openai/generated/v2/HookSource.ts create mode 100644 src/runtimes/openai/generated/v2/HookStartedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/HookTrustStatus.ts create mode 100644 src/runtimes/openai/generated/v2/HooksListEntry.ts create mode 100644 src/runtimes/openai/generated/v2/HooksListParams.ts create mode 100644 src/runtimes/openai/generated/v2/HooksListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/InAppBrowserRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/InstalledApp.ts create mode 100644 src/runtimes/openai/generated/v2/ItemCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewStartedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ItemStartedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ListMcpServerStatusParams.ts create mode 100644 src/runtimes/openai/generated/v2/ListMcpServerStatusResponse.ts create mode 100644 src/runtimes/openai/generated/v2/LoginAccountParams.ts create mode 100644 src/runtimes/openai/generated/v2/LoginAccountResponse.ts create mode 100644 src/runtimes/openai/generated/v2/LoginAppBrand.ts create mode 100644 src/runtimes/openai/generated/v2/LogoutAccountResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ManagedHooksRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/MarketplaceAddParams.ts create mode 100644 src/runtimes/openai/generated/v2/MarketplaceAddResponse.ts create mode 100644 src/runtimes/openai/generated/v2/MarketplaceInterface.ts create mode 100644 src/runtimes/openai/generated/v2/MarketplaceLoadErrorInfo.ts create mode 100644 src/runtimes/openai/generated/v2/MarketplaceRemoveParams.ts create mode 100644 src/runtimes/openai/generated/v2/MarketplaceRemoveResponse.ts create mode 100644 src/runtimes/openai/generated/v2/MarketplaceUpgradeErrorInfo.ts create mode 100644 src/runtimes/openai/generated/v2/MarketplaceUpgradeParams.ts create mode 100644 src/runtimes/openai/generated/v2/MarketplaceUpgradeResponse.ts create mode 100644 src/runtimes/openai/generated/v2/McpAuthStatus.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationArrayType.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationBooleanSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationBooleanType.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationConstOption.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationEnumSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationLegacyTitledEnumSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationMultiSelectEnumSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationNumberSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationNumberType.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationObjectType.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationPrimitiveSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationSingleSelectEnumSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationStringFormat.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationStringSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationStringType.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationTitledEnumItems.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationTitledMultiSelectEnumSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationTitledSingleSelectEnumSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationUntitledEnumItems.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationUntitledMultiSelectEnumSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpElicitationUntitledSingleSelectEnumSchema.ts create mode 100644 src/runtimes/openai/generated/v2/McpResourceReadParams.ts create mode 100644 src/runtimes/openai/generated/v2/McpResourceReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerConnectionStatus.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerElicitationAction.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerElicitationRequestParams.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerElicitationRequestResponse.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerEventNotification.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamNotification.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamStartParams.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamStartResponse.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamStopParams.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamStopResponse.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerMigration.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerOauthClientRegistration.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerOauthLoginCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerOauthLoginParams.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerOauthLoginResponse.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerRefreshResponse.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerStartupFailureReason.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerStartupState.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerStatus.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerStatusDetail.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerStatusUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerToolCallParams.ts create mode 100644 src/runtimes/openai/generated/v2/McpServerToolCallResponse.ts create mode 100644 src/runtimes/openai/generated/v2/McpToolCallAppContext.ts create mode 100644 src/runtimes/openai/generated/v2/McpToolCallError.ts create mode 100644 src/runtimes/openai/generated/v2/McpToolCallProgressNotification.ts create mode 100644 src/runtimes/openai/generated/v2/McpToolCallResult.ts create mode 100644 src/runtimes/openai/generated/v2/McpToolCallStatus.ts create mode 100644 src/runtimes/openai/generated/v2/MemoryCitation.ts create mode 100644 src/runtimes/openai/generated/v2/MemoryCitationEntry.ts create mode 100644 src/runtimes/openai/generated/v2/MemoryResetResponse.ts create mode 100644 src/runtimes/openai/generated/v2/MergeStrategy.ts create mode 100644 src/runtimes/openai/generated/v2/MigrationDetails.ts create mode 100644 src/runtimes/openai/generated/v2/MockExperimentalMethodParams.ts create mode 100644 src/runtimes/openai/generated/v2/MockExperimentalMethodResponse.ts create mode 100644 src/runtimes/openai/generated/v2/Model.ts create mode 100644 src/runtimes/openai/generated/v2/ModelAvailabilityNux.ts create mode 100644 src/runtimes/openai/generated/v2/ModelListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ModelListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadParams.ts create mode 100644 src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ModelRerouteReason.ts create mode 100644 src/runtimes/openai/generated/v2/ModelReroutedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ModelSafetyBufferingUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ModelServiceTier.ts create mode 100644 src/runtimes/openai/generated/v2/ModelUpgradeInfo.ts create mode 100644 src/runtimes/openai/generated/v2/ModelVerification.ts create mode 100644 src/runtimes/openai/generated/v2/ModelVerificationNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ModelsRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/MultiAgentVersion.ts create mode 100644 src/runtimes/openai/generated/v2/NetworkAccess.ts create mode 100644 src/runtimes/openai/generated/v2/NetworkApprovalContext.ts create mode 100644 src/runtimes/openai/generated/v2/NetworkApprovalProtocol.ts create mode 100644 src/runtimes/openai/generated/v2/NetworkDomainPermission.ts create mode 100644 src/runtimes/openai/generated/v2/NetworkPolicyAmendment.ts create mode 100644 src/runtimes/openai/generated/v2/NetworkPolicyRuleAction.ts create mode 100644 src/runtimes/openai/generated/v2/NetworkRequirements.ts create mode 100644 src/runtimes/openai/generated/v2/NetworkUnixSocketPermission.ts create mode 100644 src/runtimes/openai/generated/v2/NewThreadModelDefaults.ts create mode 100644 src/runtimes/openai/generated/v2/NonSteerableTurnKind.ts create mode 100644 src/runtimes/openai/generated/v2/OverriddenMetadata.ts create mode 100644 src/runtimes/openai/generated/v2/PatchApplyStatus.ts create mode 100644 src/runtimes/openai/generated/v2/PatchChangeKind.ts create mode 100644 src/runtimes/openai/generated/v2/PermissionGrantScope.ts create mode 100644 src/runtimes/openai/generated/v2/PermissionProfileListParams.ts create mode 100644 src/runtimes/openai/generated/v2/PermissionProfileListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PermissionProfileSummary.ts create mode 100644 src/runtimes/openai/generated/v2/PermissionsRequestApprovalParams.ts create mode 100644 src/runtimes/openai/generated/v2/PermissionsRequestApprovalResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PlanDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/PluginAuthPolicy.ts create mode 100644 src/runtimes/openai/generated/v2/PluginAvailability.ts create mode 100644 src/runtimes/openai/generated/v2/PluginDetail.ts create mode 100644 src/runtimes/openai/generated/v2/PluginDisabledReason.ts create mode 100644 src/runtimes/openai/generated/v2/PluginHookSummary.ts create mode 100644 src/runtimes/openai/generated/v2/PluginInstallParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginInstallPolicy.ts create mode 100644 src/runtimes/openai/generated/v2/PluginInstallPolicySource.ts create mode 100644 src/runtimes/openai/generated/v2/PluginInstallResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginInstalledParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginInstalledResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginInterface.ts create mode 100644 src/runtimes/openai/generated/v2/PluginListMarketplaceKind.ts create mode 100644 src/runtimes/openai/generated/v2/PluginListParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginMarketplaceEntry.ts create mode 100644 src/runtimes/openai/generated/v2/PluginReadParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSearchParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSearchResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSearchResult.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSearchScope.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareCheckoutParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareCheckoutResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareContext.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareDeleteParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareDeleteResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareDiscoverability.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareListItem.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareListParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSharePrincipal.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSharePrincipalRole.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSharePrincipalType.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareSaveParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareSaveResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareTarget.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareTargetRole.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareUpdateDiscoverability.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareUpdateTargetsParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginShareUpdateTargetsResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSkillReadParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSkillReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSource.ts create mode 100644 src/runtimes/openai/generated/v2/PluginSummary.ts create mode 100644 src/runtimes/openai/generated/v2/PluginUninstallParams.ts create mode 100644 src/runtimes/openai/generated/v2/PluginUninstallResponse.ts create mode 100644 src/runtimes/openai/generated/v2/PluginsMigration.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessExitedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessKillParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessKillResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessOutputDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessOutputStream.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessResizePtyParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessResizePtyResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessSpawnParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessSpawnResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessTerminalSize.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessWriteStdinParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProcessWriteStdinResponse.ts create mode 100644 src/runtimes/openai/generated/v2/Project.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectChangeType.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectChangedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectCreateParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectCreateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectDeleteParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectDeleteResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectImportParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectImportResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectMoveParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectMoveResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectReadParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectRoot.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectUpdateParams.ts create mode 100644 src/runtimes/openai/generated/v2/ProjectUpdateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/QueuedSubmission.ts create mode 100644 src/runtimes/openai/generated/v2/RateLimitReachedType.ts create mode 100644 src/runtimes/openai/generated/v2/RateLimitResetCredit.ts create mode 100644 src/runtimes/openai/generated/v2/RateLimitResetCreditStatus.ts create mode 100644 src/runtimes/openai/generated/v2/RateLimitResetCreditsSummary.ts create mode 100644 src/runtimes/openai/generated/v2/RateLimitResetType.ts create mode 100644 src/runtimes/openai/generated/v2/RateLimitSnapshot.ts create mode 100644 src/runtimes/openai/generated/v2/RateLimitWindow.ts create mode 100644 src/runtimes/openai/generated/v2/RawResponseCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/RawResponseItemCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ReasoningEffortOption.ts create mode 100644 src/runtimes/openai/generated/v2/ReasoningSummaryPartAddedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ReasoningSummaryTextDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ReasoningTextDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlClient.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsListOrder.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsListParams.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsRevokeParams.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsRevokeResponse.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlConnectionStatus.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlDisableParams.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlDisableResponse.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlEnableParams.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlEnableResponse.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlPairingStartParams.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlPairingStartResponse.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlPairingStatusParams.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlPairingStatusResponse.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlStatusChangedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/RemoteControlStatusReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/RequestPermissionProfile.ts create mode 100644 src/runtimes/openai/generated/v2/ResidencyRequirement.ts create mode 100644 src/runtimes/openai/generated/v2/ReviewDelivery.ts create mode 100644 src/runtimes/openai/generated/v2/ReviewStartParams.ts create mode 100644 src/runtimes/openai/generated/v2/ReviewStartResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ReviewTarget.ts create mode 100644 src/runtimes/openai/generated/v2/SandboxMode.ts create mode 100644 src/runtimes/openai/generated/v2/SandboxPolicy.ts create mode 100644 src/runtimes/openai/generated/v2/SandboxWorkspaceWrite.ts create mode 100644 src/runtimes/openai/generated/v2/ScheduledTaskSchedule.ts create mode 100644 src/runtimes/openai/generated/v2/ScheduledTaskSummary.ts create mode 100644 src/runtimes/openai/generated/v2/ScheduledTaskWeekday.ts create mode 100644 src/runtimes/openai/generated/v2/SelectedCapabilityRoot.ts create mode 100644 src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailParams.ts create mode 100644 src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ServerDiagnosticsGauge.ts create mode 100644 src/runtimes/openai/generated/v2/ServerDiagnosticsParams.ts create mode 100644 src/runtimes/openai/generated/v2/ServerDiagnosticsProcess.ts create mode 100644 src/runtimes/openai/generated/v2/ServerDiagnosticsResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ServerRequestResolvedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/SessionMigration.ts create mode 100644 src/runtimes/openai/generated/v2/SessionSource.ts create mode 100644 src/runtimes/openai/generated/v2/SkillDependencies.ts create mode 100644 src/runtimes/openai/generated/v2/SkillErrorInfo.ts create mode 100644 src/runtimes/openai/generated/v2/SkillInterface.ts create mode 100644 src/runtimes/openai/generated/v2/SkillMetadata.ts create mode 100644 src/runtimes/openai/generated/v2/SkillMigration.ts create mode 100644 src/runtimes/openai/generated/v2/SkillScope.ts create mode 100644 src/runtimes/openai/generated/v2/SkillSummary.ts create mode 100644 src/runtimes/openai/generated/v2/SkillToolDependency.ts create mode 100644 src/runtimes/openai/generated/v2/SkillsChangedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/SkillsConfigWriteParams.ts create mode 100644 src/runtimes/openai/generated/v2/SkillsConfigWriteResponse.ts create mode 100644 src/runtimes/openai/generated/v2/SkillsExtraRootsSetParams.ts create mode 100644 src/runtimes/openai/generated/v2/SkillsExtraRootsSetResponse.ts create mode 100644 src/runtimes/openai/generated/v2/SkillsListEntry.ts create mode 100644 src/runtimes/openai/generated/v2/SkillsListParams.ts create mode 100644 src/runtimes/openai/generated/v2/SkillsListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/SortDirection.ts create mode 100644 src/runtimes/openai/generated/v2/SpendControlLimitSnapshot.ts create mode 100644 src/runtimes/openai/generated/v2/StrictReviewRequiredNotification.ts create mode 100644 src/runtimes/openai/generated/v2/SubAgentActivityKind.ts create mode 100644 src/runtimes/openai/generated/v2/SubagentMigration.ts create mode 100644 src/runtimes/openai/generated/v2/TerminalInteractionNotification.ts create mode 100644 src/runtimes/openai/generated/v2/TextElement.ts create mode 100644 src/runtimes/openai/generated/v2/TextPosition.ts create mode 100644 src/runtimes/openai/generated/v2/TextRange.ts create mode 100644 src/runtimes/openai/generated/v2/Thread.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadActiveFlag.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadArchiveParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadArchiveResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadArchivedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminal.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadClosedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadCompactStartParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadCompactStartResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadDecrementElicitationParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadDecrementElicitationResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadDeleteParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadDeleteResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadDeletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadExtra.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadForkParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadForkResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoal.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoalClearParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoalClearResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoalClearedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoalGetParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoalGetResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoalSetParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoalSetResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoalStatus.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadGoalUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadHistoryMode.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadIncrementElicitationParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadIncrementElicitationResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadInjectItemsParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadInjectItemsResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadItem.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadItemEntry.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadItemsListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadItemsListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadLoadedListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadLoadedListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadMemoryModeSetParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadMemoryModeSetResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadMetadataGitInfoUpdateParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadMetadataUpdateParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadMetadataUpdateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadNameUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadProjectUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueAddParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueAddResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueChangedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueDeleteParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueDeleteResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueReorderParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueReorderResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueStartParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueStartResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueUpdateParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadQueueUpdateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadReadParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadReadResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAudioChunk.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeBemItemPresentation.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeClosedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeErrorNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeInitialItem.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItem.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItemAddedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItemCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItemStartedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItemTranscriptDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeOutputAudioDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeSdpNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeSessionOutcome.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStartParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStartResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStartTransport.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStartedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStopParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStopResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDeltaNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDoneNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptRole.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadResumeInitialTurnsPageParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadResumeParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadResumeResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRevertParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRevertResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRevertedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRollbackParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadRollbackResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSearchOccurrence.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSearchOccurrencesParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSearchOccurrencesResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSearchParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSearchResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSearchResult.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSearchSortKey.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSearchTextRange.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSection.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionAppearance.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionCreateParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionCreateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionDeleteParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionDeleteResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionMoveParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionMoveResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionUpdateParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSectionUpdateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSetNameParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSetNameResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSettings.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSettingsUpdateParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSettingsUpdateResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSettingsUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadShellCommandParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadShellCommandResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSortKey.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSource.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadSourceKind.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadStartParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadStartResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadStartSource.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadStartedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadStatus.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadStatusChangedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadTimelineEntry.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadTimelineListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadTimelineListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadTokenUsage.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadTokenUsageUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadTurnsListParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadTurnsListResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadUnarchiveParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadUnarchiveResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadUnarchivedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadUnsubscribeParams.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadUnsubscribeResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadUnsubscribeStatus.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadUsage.ts create mode 100644 src/runtimes/openai/generated/v2/ThreadUsageBreakdownGroup.ts create mode 100644 src/runtimes/openai/generated/v2/TokenUsageBreakdown.ts create mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputAnswer.ts create mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputOption.ts create mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputParams.ts create mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputQuestion.ts create mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputResponse.ts create mode 100644 src/runtimes/openai/generated/v2/ToolsV2.ts create mode 100644 src/runtimes/openai/generated/v2/Turn.ts create mode 100644 src/runtimes/openai/generated/v2/TurnCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/TurnDiffUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/TurnEnvironmentParams.ts create mode 100644 src/runtimes/openai/generated/v2/TurnError.ts create mode 100644 src/runtimes/openai/generated/v2/TurnInterruptParams.ts create mode 100644 src/runtimes/openai/generated/v2/TurnInterruptResponse.ts create mode 100644 src/runtimes/openai/generated/v2/TurnItemsView.ts create mode 100644 src/runtimes/openai/generated/v2/TurnModerationMetadataNotification.ts create mode 100644 src/runtimes/openai/generated/v2/TurnPlanStep.ts create mode 100644 src/runtimes/openai/generated/v2/TurnPlanStepStatus.ts create mode 100644 src/runtimes/openai/generated/v2/TurnPlanUpdatedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/TurnStartParams.ts create mode 100644 src/runtimes/openai/generated/v2/TurnStartResponse.ts create mode 100644 src/runtimes/openai/generated/v2/TurnStartedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/TurnStatus.ts create mode 100644 src/runtimes/openai/generated/v2/TurnSteerParams.ts create mode 100644 src/runtimes/openai/generated/v2/TurnSteerResponse.ts create mode 100644 src/runtimes/openai/generated/v2/TurnsPage.ts create mode 100644 src/runtimes/openai/generated/v2/UserInput.ts create mode 100644 src/runtimes/openai/generated/v2/WarningNotification.ts create mode 100644 src/runtimes/openai/generated/v2/WebSearchAction.ts create mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxReadiness.ts create mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxReadinessResponse.ts create mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxSetupCompletedNotification.ts create mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxSetupMode.ts create mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxSetupStartParams.ts create mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxSetupStartResponse.ts create mode 100644 src/runtimes/openai/generated/v2/WindowsWorldWritableWarningNotification.ts create mode 100644 src/runtimes/openai/generated/v2/WorkspaceMessage.ts create mode 100644 src/runtimes/openai/generated/v2/WorkspaceMessageType.ts create mode 100644 src/runtimes/openai/generated/v2/WriteStatus.ts create mode 100644 src/runtimes/openai/generated/v2/index.ts delete mode 100644 tests/acp-v1-contract-adapter-mapping.test.ts delete mode 100644 tests/acp-v1-contract-adapter-permission.test.ts delete mode 100644 tests/acp-v1-contract-adapter-run-lifecycle.test.ts delete mode 100644 tests/acp-v1-contract-adapter-terminal.test.ts create mode 100644 tests/claude-agent-sdk-durability.test.ts create mode 100644 tests/claude-agent-sdk-task-events.test.ts create mode 100644 tests/claude-agent-sdk-test-helpers.ts delete mode 100644 tests/claude-contract-adapter-permission.test.ts delete mode 100644 tests/claude-contract-adapter-run-lifecycle.test.ts delete mode 100644 tests/claude-contract-adapter-terminal.test.ts delete mode 100644 tests/claude-contract-adapter-transcript.test.ts delete mode 100644 tests/contract-projection-authority.test.ts delete mode 100644 tests/contract-projection-preview.test.ts delete mode 100644 tests/contract-projection-state.test.ts create mode 100644 tests/fixtures/architecture-boundaries-audit.ts create mode 100644 tests/fixtures/architecture-boundaries-resolution.ts create mode 100644 tests/fixtures/architecture-boundaries-runtime-loaders.ts create mode 100644 tests/fixtures/providers/claude-agent-sdk/cases/unknown-message-diagnostic.json delete mode 100644 tests/fixtures/providers/claude-agent-sdk/cases/unknown-message-ignored.json create mode 100644 tests/fixtures/public-api-spaces-source.ts create mode 100644 tests/fixtures/public-api-type-namespace.ts create mode 100644 tests/fixtures/public-api-type-star-source.ts create mode 100644 tests/openai-app-server-event-bridge-fixture.ts delete mode 100644 tests/openai-app-server-turn-start.test.ts delete mode 100644 tests/openai-contract-adapter-interactions.test.ts delete mode 100644 tests/openai-contract-adapter-items.test.ts delete mode 100644 tests/openai-contract-adapter-run-lifecycle.test.ts delete mode 100644 tests/openai-contract-adapter-terminal.test.ts create mode 100644 tests/remote-http-mcp-executor.test.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9538bf4..23fb47a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -24,7 +24,7 @@ jobs: pull-requests: read steps: - name: Validate PR title - uses: amannn/action-semantic-pull-request@v6 + uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -48,12 +48,12 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 + uses: voidzero-dev/setup-vp@313600b80b104eadebb9111787d37a2e83e014ca # v1.17.0 with: cache: true run-install: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 008d583..fb5b184 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,52 +8,81 @@ on: permissions: {} concurrency: - group: release-${{ github.ref }} - cancel-in-progress: false + group: release + queue: max jobs: - build: - name: Build + verify: + name: Verify release runs-on: ubuntu-latest permissions: contents: read outputs: - version: ${{ steps.verify.outputs.version }} + version: ${{ steps.release.outputs.version }} steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false + ref: ${{ github.sha }} - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - cache: false - run-install: | - - args: ['--frozen-lockfile'] - - - id: verify + - id: release name: Verify release tag run: | set -euo pipefail tag="${GITHUB_REF_NAME}" version="$(node -p "require('./package.json').version")" - release_commit="$(git rev-parse "${tag}^{commit}")" if [[ "${tag}" != "v${version}" ]]; then echo "::error::Tag ${tag} does not match package version ${version}." exit 1 fi - if ! git merge-base --is-ancestor "${release_commit}" origin/main; then - echo "::error::Tag ${tag} must point to a commit reachable from origin/main." + if [[ ! "${version}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "::error::Release version must be canonical stable MAJOR.MINOR.PATCH: ${version}." + exit 1 + fi + + if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then + echo "::error::Release commit ${GITHUB_SHA} must be reachable from origin/main." exit 1 fi printf 'version=%s\n' "${version}" >> "${GITHUB_OUTPUT}" + build: + name: Build + needs: verify + runs-on: ubuntu-latest + permissions: + attestations: write + contents: read + id-token: write + outputs: + artifact_attempt: ${{ github.run_attempt }} + digest: ${{ steps.image.outputs.digest }} + version: ${{ needs.verify.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@313600b80b104eadebb9111787d37a2e83e014ca # v1.17.0 + with: + cache: false + run-install: | + - args: ['--frozen-lockfile'] + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24.19.0" + - name: Check run: vp run check @@ -63,24 +92,38 @@ jobs: npm pack --ignore-scripts --pack-destination package - name: Test packed driver - timeout-minutes: 180 env: + AGENT_DRIVER_LIVE: "1" AGENT_DRIVER_LIVE_ARTIFACT: packed/dist/driver.mjs - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} run: | + set -euo pipefail + + tarballs=(package/*.tgz) + if [[ "${#tarballs[@]}" -ne 1 ]]; then + echo "::error::Expected exactly one npm package." + exit 1 + fi + mkdir packed - tar -xzf package/*.tgz -C packed --strip-components=1 - vp run test:live:artifact + tar -xzf "${tarballs[0]}" -C packed --strip-components=1 + shopt -s globstar nullglob + declarations=(packed/dist/types/**/*.d.ts) + vp exec tsc --ignoreConfig --noEmit --moduleResolution Bundler --module ESNext --target ESNext "${declarations[@]}" + bun test tests/driver-artifact-mcp.test.ts - - name: Build image + - id: image + name: Build image env: - VERSION: ${{ steps.verify.outputs.version }} + VERSION: ${{ needs.verify.outputs.version }} run: | + set -euo pipefail + title="$(node -p "require('./package.json').name")" description="$(node -p "require('./package.json').description")" license="$(node -p "require('./package.json').license")" buildah build \ + --http-proxy=false \ --platform linux/amd64 \ --label "org.opencontainers.image.title=$title" \ --label "org.opencontainers.image.description=$description" \ @@ -92,70 +135,258 @@ jobs: . buildah push localhost/agent-driver oci-archive:image.oci.tar + image_digest="$(skopeo inspect --format '{{.Digest}}' oci-archive:image.oci.tar)" + if [[ ! "$image_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "::error::Invalid image digest: $image_digest" + exit 1 + fi + printf 'digest=%s\n' "$image_digest" >> "$GITHUB_OUTPUT" + + - name: Test image environment + run: podman run --pull=never --rm --entrypoint node localhost/agent-driver /usr/local/libexec/mosoo/environment-package-manager-check.mjs smoke + + - name: Attest image + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.image.outputs.digest }} + - name: Upload package - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: npm-package + name: npm-package-${{ github.run_attempt }} path: package/*.tgz if-no-files-found: error - retention-days: 1 + retention-days: 31 - name: Upload image - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: container-image + name: container-image-${{ github.run_attempt }} path: image.oci.tar if-no-files-found: error retention-days: 1 compression-level: 0 - publish-image: - name: Publish image + publish-versioned-image: + name: Publish versioned image needs: build runs-on: ubuntu-latest env: IMAGE: ghcr.io/${{ github.repository }} VERSION: ${{ needs.build.outputs.version }} + outputs: + digest: ${{ steps.publish.outputs.digest }} + version: ${{ steps.publish.outputs.version }} permissions: + attestations: read + contents: read packages: write steps: - name: Download image - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: container-image + name: container-image-${{ needs.build.outputs.artifact_attempt }} - - name: Publish image + - id: publish + name: Publish versioned image env: + DOCKER_CONFIG: ${{ runner.temp }}/docker + EXPECTED_DIGEST: ${{ needs.build.outputs.digest }} GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REGISTRY_AUTH_FILE: ${{ runner.temp }}/docker/config.json run: | - printf '%s' "$GHCR_TOKEN" | skopeo login --username "$GITHUB_ACTOR" --password-stdin ghcr.io - skopeo copy oci-archive:image.oci.tar "docker://$IMAGE:$VERSION" - skopeo copy oci-archive:image.oci.tar "docker://$IMAGE:latest" + set -euo pipefail + + printf '%s' "$GHCR_TOKEN" | docker login --username "$GITHUB_ACTOR" --password-stdin ghcr.io + + if remote_digest="$(skopeo inspect --format '{{.Digest}}' "docker://$IMAGE:$VERSION" 2>inspect.err)"; then + echo "$IMAGE:$VERSION already exists at $remote_digest." + else + inspect_error="$(&2 + exit 1 + ;; + esac + + archive_digest="$(skopeo inspect --format '{{.Digest}}' oci-archive:image.oci.tar)" + if [[ "$archive_digest" != "$EXPECTED_DIGEST" ]]; then + echo "::error::Downloaded image digest $archive_digest does not match attested build $EXPECTED_DIGEST." + exit 1 + fi + + skopeo copy --preserve-digests oci-archive:image.oci.tar "docker://$IMAGE:$VERSION" + remote_digest="$(skopeo inspect --format '{{.Digest}}' "docker://$IMAGE:$VERSION")" + if [[ "$remote_digest" != "$EXPECTED_DIGEST" ]]; then + echo "::error::Published image digest $remote_digest does not match attested build $EXPECTED_DIGEST." + exit 1 + fi + fi + + if [[ ! "$remote_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "::error::Invalid remote image digest: $remote_digest" + exit 1 + fi + + gh attestation verify "oci://$IMAGE@$remote_digest" \ + --repo "$GITHUB_REPOSITORY" \ + --cert-identity "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/.github/workflows/release.yml@$GITHUB_REF" \ + --source-digest "$GITHUB_SHA" \ + --source-ref "$GITHUB_REF" \ + --deny-self-hosted-runners + printf 'digest=%s\n' "$remote_digest" >> "$GITHUB_OUTPUT" + printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT" publish-npm: name: Publish to npm - needs: build + needs: [build, publish-versioned-image] runs-on: ubuntu-latest environment: name: npm url: https://www.npmjs.com/package/@mosoo/agent-driver/v/${{ needs.build.outputs.version }} permissions: id-token: write + outputs: + promote_latest: ${{ steps.publish.outputs.promote_latest }} steps: - name: Download package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: npm-package + name: npm-package-${{ needs.build.outputs.artifact_attempt }} path: package - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "lts/*" + node-version: "24.19.0" registry-url: https://registry.npmjs.org - - name: Publish package - run: npm publish package/*.tgz --ignore-scripts - # Bootstrap only: remove this env block after configuring npm Trusted Publishing. + - id: publish + name: Publish package + # Bootstrap only: remove NODE_AUTH_TOKEN after configuring npm Trusted Publishing. env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + VERSION: ${{ needs.build.outputs.version }} + run: | + set -euo pipefail + + tarballs=(./package/*.tgz) + if [[ "${#tarballs[@]}" -ne 1 ]]; then + echo "::error::Expected exactly one npm package." + exit 1 + fi + + tarball="${tarballs[0]}" + package_name="$(tar -xOf "$tarball" package/package.json | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).name")" + local_integrity="$(node -e "const { createHash } = require('node:crypto'); const { readFileSync } = require('node:fs'); process.stdout.write('sha512-' + createHash('sha512').update(readFileSync(process.argv[1])).digest('base64'));" "$tarball")" + + if remote_integrity="$(npm view "$package_name@$VERSION" dist.integrity 2>view.err)"; then + if [[ "$remote_integrity" != "$local_integrity" ]]; then + echo "::error::Refusing to replace $package_name@$VERSION ($remote_integrity) with $local_integrity." + exit 1 + fi + + echo "$package_name@$VERSION already contains $local_integrity." + publish_needed=false + else + view_error="$(&2 + exit 1 + ;; + esac + + publish_needed=true + fi + + promote_latest=true + if newer_versions="$(npm view "$package_name@>$VERSION" version --json 2>newer.err)"; then + if [[ "$publish_needed" == true ]]; then + echo "::error::Refusing to publish $package_name@$VERSION after newer versions: $newer_versions" + exit 1 + fi + promote_latest=false + else + newer_error="$(&2 + exit 1 + ;; + esac + fi + + if [[ "$publish_needed" == true ]]; then + npm publish "$tarball" --ignore-scripts --provenance + fi + printf 'promote_latest=%s\n' "$promote_latest" >> "$GITHUB_OUTPUT" + + publish-latest-image: + name: Publish latest image + needs: [publish-versioned-image, publish-npm] + if: needs.publish-npm.outputs.promote_latest == 'true' + runs-on: ubuntu-latest + env: + IMAGE: ghcr.io/${{ github.repository }} + DIGEST: ${{ needs.publish-versioned-image.outputs.digest }} + PACKAGE_NAME: "@mosoo/agent-driver" + VERSION: ${{ needs.publish-versioned-image.outputs.version }} + permissions: + packages: write + steps: + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24.19.0" + + - name: Verify npm latest + run: | + set -euo pipefail + + if npm_latest="$(npm view "$PACKAGE_NAME" dist-tags.latest 2>latest.err)"; then + if [[ "$npm_latest" != "$VERSION" ]]; then + echo "::error::Refusing to move $IMAGE:latest to $VERSION while npm latest is $npm_latest." + exit 1 + fi + else + latest_error="$(&2 ;; + esac + exit 1 + fi + + if newer_versions="$(npm view "$PACKAGE_NAME@>$VERSION" version --json 2>newer.err)"; then + echo "::error::Refusing to move $IMAGE:latest to $VERSION after newer npm versions: $newer_versions" + exit 1 + else + newer_error="$(&2 + exit 1 + ;; + esac + fi + + - name: Publish latest image + env: + DOCKER_CONFIG: ${{ runner.temp }}/docker + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REGISTRY_AUTH_FILE: ${{ runner.temp }}/docker/config.json + run: | + set -euo pipefail + + printf '%s' "$GHCR_TOKEN" | docker login --username "$GITHUB_ACTOR" --password-stdin ghcr.io + skopeo copy --preserve-digests "docker://$IMAGE@$DIGEST" "docker://$IMAGE:latest" diff --git a/Containerfile b/Containerfile index c3bfdb8..f0a4fba 100644 --- a/Containerfile +++ b/Containerfile @@ -1,10 +1,16 @@ -FROM cloudflare/sandbox:0.12.3 +ARG BUN_VERSION=1.4.0 +FROM docker.io/oven/bun:${BUN_VERSION}@sha256:5ff609364c049b54eb0ff560ec96319729a972078ef2c755d758f0c6ef89c2d6 AS bun-runtime -# Keep the default base image version in sync with apps/api/package.json -> @cloudflare/sandbox. -ARG CLAUDE_AGENT_SDK_VERSION=0.3.211 -ARG ANTHROPIC_SDK_VERSION=0.111.0 -ARG OPENAI_RUNTIME_VERSION=0.144.5 -ARG OPENCODE_VERSION=1.18.4 +FROM docker.io/cloudflare/sandbox:0.12.8@sha256:822501de5f0c52a012c125c4e5e4c0080421a8e93ca4ce0ba3d247148021989f + +# Keep this pin in sync with downstream mosoo apps/api/package.json -> @cloudflare/sandbox. +ARG CLAUDE_AGENT_SDK_VERSION=0.3.247 +ARG BUN_VERSION +ARG OPENAI_RUNTIME_VERSION=0.150.1 +ARG OPENCODE_VERSION=1.18.23 + +COPY --from=bun-runtime /usr/local/bin/bun /usr/local/bin/bun +RUN test "$(bun --version)" = "$BUN_VERSION" # Install the Python runtime behind writable pip package declarations. RUN apt-get update \ @@ -12,7 +18,7 @@ RUN apt-get update \ python3 \ python3-pip \ python-is-python3 \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/cache/apt/* /var/lib/apt/lists/* COPY environment-package-managers.json /etc/mosoo/environment-package-managers.json COPY scripts/environment-package-manager-check.mjs /usr/local/libexec/mosoo/environment-package-manager-check.mjs @@ -26,30 +32,23 @@ RUN node /usr/local/libexec/mosoo/environment-package-manager-check.mjs verify # Installed in a single npm invocation to keep the agent packages in one layer. # # Package -> binary -> runtime: -# @anthropic-ai/claude-agent-sdk -> native claude -> claude-agent-sdk +# Claude native package -> claude -> claude-agent-sdk # OpenAI app-server package -> OpenAI CLI -> openai-runtime -# opencode-ai -> opencode -> acp-fallback -# bun (base image) -> bun -> driver launcher +# OpenCode baseline package -> opencode -> acp-fallback +# bun (bun-runtime stage) -> bun -> driver launcher # -# Pick the architecture-specific `claude` binary that npm just installed under -# `@anthropic-ai/claude-agent-sdk-` so the image works -# on CF Containers (linux/amd64) and local arm64 hosts (e.g. Apple Silicon) -# without forcing platform emulation. -RUN OPENAI_RUNTIME_PACKAGE="@openai/codex@${OPENAI_RUNTIME_VERSION}" \ - && npm install -g \ - @anthropic-ai/claude-agent-sdk@${CLAUDE_AGENT_SDK_VERSION} \ - @anthropic-ai/sdk@${ANTHROPIC_SDK_VERSION} \ - opencode-ai@${OPENCODE_VERSION} \ - "$OPENAI_RUNTIME_PACKAGE" \ +RUN npm install -g --ignore-scripts \ + @anthropic-ai/claude-agent-sdk-linux-x64@${CLAUDE_AGENT_SDK_VERSION} \ + opencode-linux-x64-baseline@${OPENCODE_VERSION} \ + @openai/codex@${OPENAI_RUNTIME_VERSION} \ + && ln -s /usr/local/lib/node_modules/opencode-linux-x64-baseline/bin/opencode /usr/local/bin/opencode \ + && ln -s /usr/local/lib/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64/claude /usr/local/bin/mosoo-claude-code \ && codex --version \ && codex app-server --help >/dev/null \ && opencode --version \ && opencode acp --help >/dev/null \ - && CLAUDE_ARCH_PACKAGE="$(node -p "'@anthropic-ai/claude-agent-sdk-' + (process.arch === 'arm64' ? 'linux-arm64' : 'linux-x64')")" \ - && CLAUDE_BIN="/usr/local/lib/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/${CLAUDE_ARCH_PACKAGE}/claude" \ - && test -x "$CLAUDE_BIN" \ - && ln -sf "$CLAUDE_BIN" /usr/local/bin/mosoo-claude-code \ - && npm cache clean --force + && mosoo-claude-code --version \ + && rm -rf /root/.npm ENV MOSOO_CLAUDE_CODE_EXECUTABLE=/usr/local/bin/mosoo-claude-code ENV MOSOO_ACP_FALLBACK_COMMAND=opencode diff --git a/README.md b/README.md index 3f7dd3a..e765cb9 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,9 @@ vp run check vp run clean ``` -`vp run build:image` uses Buildah to produce a local `agent-driver:local` OCI image and installs `dist/driver.mjs` on the image `PATH` as `agent-driver`. +`vp run build:image` uses Buildah to produce a local linux/amd64 `agent-driver:local` OCI image and installs `dist/driver.mjs` on the image `PATH` as `agent-driver`. -The image contract in `environment-package-managers.json` exposes `npm` and `pip` to Mosoo Environment writes. The image build verifies that each tool is executable, reports a valid version, and resolves through coherent Python/pip aliases. `vp run docker:smoke:environment` installs and executes one pinned package through each manager using the same isolated-prefix mode as Mosoo Environment artifacts. +The image contract in `environment-package-managers.json` exposes `npm` and `pip` to Mosoo Environment writes. The image build verifies that each tool is executable, reports a valid version, and resolves through coherent Python/pip aliases. `vp run test:image:environment` installs and executes one pinned package through each manager using the same isolated-prefix mode as Mosoo Environment artifacts. ## Boundaries @@ -176,7 +176,7 @@ The image contract in `environment-package-managers.json` exposes `npm` and `pip - `vp run check` - `vp run build:image` -- `vp run docker:smoke:environment` +- `vp run test:image:environment` - no `@mosoo/*` runtime dependencies in `package.json` - public entries include typed exports - live artifact tests are gated by environment credentials @@ -215,7 +215,7 @@ Protocol-only races such as ACP load replay barriers, burst updates, and event-d - `vp run test:live:opencode` runs all configured OpenCode compatibility models plus one representative lifecycle model. - `vp run test:live:artifact` tests the artifact path supplied by `AGENT_DRIVER_LIVE_ARTIFACT` without rebuilding it. -The release workflow extracts the packed NPM archive to `packed/` and blocks image and package publication unless `packed/dist/driver.mjs` passes the complete matrix. +The release workflow extracts the packed NPM archive to `packed/` and blocks image and package publication unless `packed/dist/driver.mjs` passes the provider-free MCP artifact test. ## License diff --git a/bun.lock b/bun.lock index 035bb32..59f6602 100644 --- a/bun.lock +++ b/bun.lock @@ -1,51 +1,51 @@ { - "lockfileVersion": 1, + "lockfileVersion": 2, "configVersion": 1, "workspaces": { "": { "name": "@mosoo/agent-driver", "dependencies": { - "@agentclientprotocol/sdk": "1.2.1", - "@anthropic-ai/claude-agent-sdk": "0.3.211", - "@anthropic-ai/sdk": "0.111.0", - "@modelcontextprotocol/client": "^2.0.0-alpha.2", - "@orpc/client": "^1.14.3", + "@agentclientprotocol/sdk": "1.4.0", + "@anthropic-ai/claude-agent-sdk": "0.3.247", + "@anthropic-ai/sdk": "0.121.0", + "@modelcontextprotocol/client": "^2.0.0", + "@orpc/client": "^1.15.0", "fflate": "^0.8.3", - "vestig": "^0.23.0", + "vestig": "^0.24.1", "zod": "^4.4.3", }, "devDependencies": { - "@openai/codex-sdk": "0.144.5", - "@types/bun": "1.3.14", - "@types/node": "^25.8.0", - "opencode-ai": "1.18.4", - "typescript": "^6.0.3", - "vite-plus": "0.2.5", + "@openai/codex": "0.150.1", + "@types/bun": "1.4.0", + "@types/node": "^26.3.0", + "opencode-ai": "1.18.23", + "typescript": "^7.0.2", + "vite-plus": "0.3.0", }, }, }, "packages": { - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.2.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.4.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-/eufudw+aFY1LKLolT6yFE6UMmYRl7fMJ/DEONSIyR6wI3slHWITBsANRGqXEY8FRzqUxwh7QEaGiZHcJPVThg=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.211", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.211", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.211", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.211", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.211", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.211", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.211", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.211", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.211" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-JhbLu6o1v2g9fjqkO+LDNPWrE0bgd9UeRQQ41JBGouAgows3KyPPYgU2WU0q7M2onuwQxR5plGDpas01F+oaUA=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.247", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.247", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.247", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.247", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.247", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.247", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.247", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.247", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.247" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-T9BDPloZxABkGREDRbWA7TVa9SLBMWI5GQXw39+XotQLCIX3McRic9OJdUnAdXa7sutwQ4nNrXwTSazN4IphjA=="], - "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.211", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Iwhm4kfcs20LdXffZ2RGRjj+BFdUOrT/JjhGtICjlGlBPlrLkkkAiHtGzqO9K36v5B/kSIHwOw9CM836kYYPHQ=="], + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.247", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vvCMAXy2KmiGHbJuThv+W6a+49qjofMR6S71FM8FTbkAAKg62cYcEyzVMVM3pNpBM5PtkMe8lRCqKnT9NUo6ng=="], - "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.211", "", { "os": "darwin", "cpu": "x64" }, "sha512-sMBW1CLe2Hq4PwwvEbz9r8LxF84UErgB45TSf+iEa8M/EjYZCsXSoTRT0vKnN4TvrAN5mWoKgi39dkUxS9ybsA=="], + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.247", "", { "os": "darwin", "cpu": "x64" }, "sha512-742ZXrdAxHvw+vzMFJMMvfbH8mWWCBajQH27BT+3fQU3Frjqu9gXKvA63dvQgxwjqO1gYU9TSy/A6PKq7zujaQ=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.211", "", { "os": "linux", "cpu": "arm64" }, "sha512-orZm8p+BzVRZ7I8c5yD43hEZ5TvBZ+UbKTZTlvID00Y8HSn3M3rNX3sW4RUvNGF2e0eNMaXKysPyEHPEj3kqpg=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.247", "", { "os": "linux", "cpu": "arm64" }, "sha512-5/3ZQ9vOwrLv6w0CFjdbaTxPFyabHNLAEojkeiYmKzqlZ/taXG58/kVKgjqgx2qQhjcHCzy9JXDw5kp3RKJoGw=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.211", "", { "os": "linux", "cpu": "arm64" }, "sha512-X1eg+lCwNH2VXyqLQR722dsDDtWPfUnz7OtnmPVoNMxcMexDlSLMMuvm5fNNi94kYT0pxmBdhIvudew145JuHg=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.247", "", { "os": "linux", "cpu": "arm64" }, "sha512-8UNRQcSy9lpLhknXryid5cTfxHxBZft4oK5R0XIIZUXY/tXvsNP3+kVYAHQUNyc1J8IDa15sd19iKkMJMqFodA=="], - "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.211", "", { "os": "linux", "cpu": "x64" }, "sha512-ohDS5EGKQvKiUUMtDNPjyWUDvaeIa+DlzUVjrZ8Y4hPtoWFpvOBtOFIYChJGpllwZ4YULS4H3gywVnLGB4do6Q=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.247", "", { "os": "linux", "cpu": "x64" }, "sha512-+tEIHgblLvBF+datSZb9WwnODXsFI7JxSFvm++oliH0eQO5zGDq0LD0hbY6h/A3nqODnBbIwVVBykwXMn68GPg=="], - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.211", "", { "os": "linux", "cpu": "x64" }, "sha512-cR12YFMVGSj38074OYkkjgwhYeblgVRK3Uw7ZXw3ZTOevjQLASPajVruFuDRW07ixxTa1ZWxqI3kSsIl71RXYA=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.247", "", { "os": "linux", "cpu": "x64" }, "sha512-iMLjkVHbcxQ5WY2WwieOJK57kD4c8Cc2tpHwpEmdTDsnWLfmNt3XFuJdmeROwyra+tcVA52oHL0dWr51+V50Pg=="], - "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.211", "", { "os": "win32", "cpu": "arm64" }, "sha512-AOIQRFO0YMDUCrG8W0NUWitBQQUB6fYdNW3SMcPPq3mXpYC/pCNURFyvRdrFm729xXkxDAP5OMLk9x2/IFrTmA=="], + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.247", "", { "os": "win32", "cpu": "arm64" }, "sha512-Sni6U2xR55bUEJRv7Eikm6oC27IOn3rlVJZAzXRFCAgkf9+aBqq/dEUJiIcRT2xbXOIe5QdPGUmpnYfU1BsHMw=="], - "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.211", "", { "os": "win32", "cpu": "x64" }, "sha512-pwzNuJg2xRBsv3kSSVVhgLdGAFxd5DqPkQX5ZLrT4uBZDJ+QM4xWKZyN8ZoFLLzNl+u0/4U83Q1ZQ0NdG/9JsQ=="], + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.247", "", { "os": "win32", "cpu": "x64" }, "sha512-yWfZo2ER3EIS/3pv6EX1M/VrZXu+zFzKhgWJjGgFSlcupbaCAxrVBRTdCyfv5UYBKWH9kl5HjgV2HhLHq67LSw=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.111.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-1hUqKi+uJQoS5X90+InwHbFAXMvgq0DnsC5hVLEeSRaODiU5WvmqDAcVCmGS2wC0pN9Z8jtWCbWw7JLzeDdm/Q=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.121.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WFzwcH8l49CZHv0vQ9QQT51VJ1/DLDQfNSs9awpGlnKBdaSnqtdZa+tw/3cxxosTPIgK8uw1GqsJ2dSbCBD1Lg=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], @@ -55,175 +55,165 @@ "@blazediff/core": ["@blazediff/core@1.9.1", "", {}, "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA=="], - "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], - - "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - - "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0-alpha.2", "", { "dependencies": { "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-FxlR5QyBPeCDEDPH2Kx20uygmuy9k2jh6ahUeEYtmVfUxboZZlUEUhn6w0XxnbxpkELcT1qyzTXC8Bqh3c8QUA=="], + "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], - "@openai/codex": ["@openai/codex@0.144.5", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.144.5-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.144.5-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.144.5-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.144.5-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.144.5-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.144.5-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-jjB+K+OMv572mKhS+2QuLxWXDJNdpwbPenf+V+8bdq7wg4Scqt3cn6WEekD8wPqDVZqck0HSX17K9rD9kbDJQA=="], + "@openai/codex": ["@openai/codex@0.150.1", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.150.1-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.150.1-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.150.1-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.150.1-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.150.1-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.150.1-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-knrbhpJH3mEULAVStcZW4F5WEt9MQhBj6KFOonBSIUGTLcHlu9CE7FRmr95E33y94+sWNZSeVBBV/kYvlfgxkQ=="], - "@openai/codex-darwin-arm64": ["@openai/codex@0.144.5-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zcT6NfBCqLFt+BReNSETTZW6v6PdbH0dzNtm9j7l7mDGqwPbKZDGJdnpkBao2389I0ZacyIKgSZoI0vez1d4Dw=="], + "@openai/codex-darwin-arm64": ["@openai/codex@0.150.1-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z614kKSI3/p+YMotBJMxCc4kvRCYEgsVmnaCG6U8HDraqTFxYcQYQ79B4/GBCBS88/KtacHI6qzjA+4Mu5BynA=="], - "@openai/codex-darwin-x64": ["@openai/codex@0.144.5-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-//Mo0m1MwaoT6psu5xsmofXpKx4/0irIkeq10xJvk59+886EG355ibjA+ZmlRcKhE3bLjsKD7p81nTbAdRL/bw=="], + "@openai/codex-darwin-x64": ["@openai/codex@0.150.1-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-vcvJ7Q4IP2vA5ZR3v9Pj9VLKtlhD7efujxHqqW/NJ8F2/XqTD4iPjErV07190Om3wONW48yploTgul16QUk9Mg=="], - "@openai/codex-linux-arm64": ["@openai/codex@0.144.5-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-zAHggxVwR2TBxKmybXY7ZMiB0G8DMonY2YPdwNNjwXcf+LOIqNGgswwNCDMbP/HEe6r8j+R9ZX/yYoo8f+n/RQ=="], + "@openai/codex-linux-arm64": ["@openai/codex@0.150.1-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-U0BY5UDsCy1up/O2YzGtVtDbcumNerVyZVVx55PW9gGQsFOL0utOyBJiG/UiocFAXix8JNvNJxYMRt2Jx0FJKg=="], - "@openai/codex-linux-x64": ["@openai/codex@0.144.5-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-FalLJlBQGFdK8Gc3kj9sa/ekNdgkHhUawLaKkvy5CtB18JaP2YxtTP/Pe1pD2iBiq8mMUliRnafpF6AdBdQMbg=="], + "@openai/codex-linux-x64": ["@openai/codex@0.150.1-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-B3Bu5MF/G9qFhbSMrdyZz9ocknXOo45DnOdWrREJaxWTjiuUO1ogoDR0/ttdc29JRFkbbXs3aC+Td8vIx3X0oA=="], - "@openai/codex-sdk": ["@openai/codex-sdk@0.144.5", "", { "dependencies": { "@openai/codex": "0.144.5" } }, "sha512-90wHPEGyk74On6gwQPNtw+wzuDJ2zYpiVADDxw43S1cWn3QbBh/21zFS2xlAWWCrdY0gE90yXfmmrzwdUDlBGw=="], + "@openai/codex-win32-arm64": ["@openai/codex@0.150.1-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-8I7N3ppKS1ql9GUr2RHS5Gw+KkRCKfIMIDDK/brxq6HizLgHoDK4CIR0OTvEgdX3Yh/2reBbxr9P4L1e+61e+Q=="], - "@openai/codex-win32-arm64": ["@openai/codex@0.144.5-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-0Pj7iqjEOEvPQPO3kFfCy9vGX4BTu76ChFFZHr2eNNIfVc3FOENAv/X98u4L+iIUtDOK9DbqmfUudW3DPapshg=="], + "@openai/codex-win32-x64": ["@openai/codex@0.150.1-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-1P1tscFnw4A4ip/WN1R5WYPdfuAlxLctPpcE1QAPQnOtbkJR5NX8da4kNTQmiDN/Flx3TLJPDTRHP9Jn435eJg=="], - "@openai/codex-win32-x64": ["@openai/codex@0.144.5-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-DnsSTlnnzleTxvLwIGnBitKInscxn2I7qASqosS8Fv+qysBygd+ZiBn/SQsRCgQ28PAlsNzmd3Gf3ZTecolAmg=="], + "@orpc/client": ["@orpc/client@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0", "@orpc/standard-server-fetch": "1.15.0", "@orpc/standard-server-peer": "1.15.0" } }, "sha512-Qt0FdPSGySdQwy83iUWOq+Iqhw2gM9qHtyxfBbcw1mwOz1s4O2BwUFA3ymVTLIRRNYRgPrQLaBZhp8CacqeePg=="], - "@orpc/client": ["@orpc/client@1.14.6", "", { "dependencies": { "@orpc/shared": "1.14.6", "@orpc/standard-server": "1.14.6", "@orpc/standard-server-fetch": "1.14.6", "@orpc/standard-server-peer": "1.14.6" } }, "sha512-Y03NcTtmEJdxcqkKBkdGxqe1IHVpD9IorshG4PaTnz9dQIW+RYI8anRo7o0IlbBlzBICN+Ubo1rnw6bpkhagCQ=="], + "@orpc/shared": ["@orpc/shared@1.15.0", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-A3/JE7pQYSrrRm6/WYJxV3GBhpMdJRPM3h47slQtWBUZe9Sao5En5WBc4tISGQNP2emcez6qIvDziSgD2T+img=="], - "@orpc/shared": ["@orpc/shared@1.14.6", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-P2W+DdrUq18kUiF7nIw5wDOA0SR41mM/NsVKDVRsdyhdHk9V9KDuW1JRymyMl+7Wo5SDeSr1Rm/VjA5v08+PHw=="], + "@orpc/standard-server": ["@orpc/standard-server@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0" } }, "sha512-bje/xn6thDqJY/JQ7xoOjmD1KWE4FsyZOPgnPxwqTZLx/r00stRhLuFvk1hDEGj9UlG7NLZEijDVx0wqvcyDzA=="], - "@orpc/standard-server": ["@orpc/standard-server@1.14.6", "", { "dependencies": { "@orpc/shared": "1.14.6" } }, "sha512-75Oh4rAZb8K7P46d6v7R2JhjqjLLEo7Qs4+ABdF+f0m0uKM1oaykJWy7leqTJ+WaYm+uDbsZl/3nyWie9aeJTg=="], + "@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0" } }, "sha512-XYVfgmIt71YrPJSI7RKRiNWjcyktPYHpevkjeohGNBP5aSMAUL95quZArAWr+XOPp4U56XRao54I6wFMZa4How=="], - "@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.14.6", "", { "dependencies": { "@orpc/shared": "1.14.6", "@orpc/standard-server": "1.14.6" } }, "sha512-XnwEnHaKMMBoilK0QVwgMsUvDbCXyz6CDoLgMN51v+p3VSnwjIkrMdOwbc/sj43z6T4irQDu9Zm8T1pxxh1L8w=="], + "@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0" } }, "sha512-fsTN+FrdPkseVx99yRenGJYSp0xOrEk9fODk8oh3zzricNYzlZ8GGgCYtDNgt9vFIR/J05dc+Nk76KhcPBQpLw=="], - "@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.14.6", "", { "dependencies": { "@orpc/shared": "1.14.6", "@orpc/standard-server": "1.14.6" } }, "sha512-jwVGc5yRA5hk1F/W4yw45P2H4fbu4Tfsk62XijJBXRvtlSNKvvPAuwAd6jFv/fxnq2SH/uskgi91Ja9wgfnYDQ=="], + "@oxc-project/runtime": ["@oxc-project/runtime@0.146.0", "", {}, "sha512-lbXHIpZ1MmK6zuw5txlMdIZ2waLVUIU5Gnm3sEuwJOiqDfQfbtjeHscatmeBoxbv8+If9LFM6PGh/3DcDWYIYw=="], - "@oxc-project/runtime": ["@oxc-project/runtime@0.139.0", "", {}, "sha512-WnuGdceWtRdqD7f3alOIDXN6bnGuGtFjtQf/dHjzgn2im7eKaYRJTEl2T1kFEWPhBWCDk+UDYgsTLUE5L6jc0w=="], + "@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], - "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.64.0", "", { "os": "android", "cpu": "arm" }, "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ=="], - "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.58.0", "", { "os": "android", "cpu": "arm" }, "sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg=="], + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.64.0", "", { "os": "android", "cpu": "arm64" }, "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw=="], - "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.58.0", "", { "os": "android", "cpu": "arm64" }, "sha512-rD0lRaJp1b+9vw6X4A2dJWKukd6X8yxiicN4JxXcXayolmUypRZxk+lKR+fVOu5q/iYc0fh5fR4bgmfOfVlbaA=="], + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.64.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw=="], - "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.58.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uzbPPk7O6M+w2K65vcQ1woga3wgP8zghjL1KOG5b6qJ8dvYHZJ1VShaslg2KOK6yQIwCQtcMCXqLBM6sqXUNTg=="], + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.64.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA=="], - "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.58.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-L0nKYDxU32oxeQqJj21W9SlIMnf81VZEhyah6iDvFhf5q0oynq498Fopth7blErUJVBpVtxQ98RMCfMPqpJX6w=="], + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.64.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ=="], - "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.58.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-woNwfD58dC5PGS9LSLSD5JYfo/EFK5iG9vhDWkcCg3q78ag7KC8bpDqgvPHrMoXpx83OLXxoSOhu6z8FsVTHlg=="], + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.64.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg=="], - "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sqs8nMLxuQpY21NKJ1u4stPDmO5hskBCNNh2E3AdCfI1QqWtf4m+Qn4mGEIUO4KGmuq3SWc/SZ80uy5IiwTCDw=="], + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.64.0", "", { "os": "linux", "cpu": "arm" }, "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w=="], - "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Vd4exzBI5B5hB9m22JiTQzIL23WvHo/Pe+sNXPNeBLXSP9swCBPKCEBRwKpmpQzYhlgYaCgfPcGXPKAJBRIiZQ=="], + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.64.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w=="], - "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-bUWi5mHV+4Vi56RLHE1h6q/HHfwAIT3XoB9vJAVeRzfu5NriXM8y6eeJu0vlKa0C9kq2rq1sOWRClhdLHPocrg=="], + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.64.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw=="], - "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ZHxemzgHcjtktAuVUwSoyXmGo/t+aF5tS1ciPpPei4rhSyrz3JOqDosXXrmhN/yLUSzJjtuW7ToTWqfQpCj2w=="], + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.64.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw=="], - "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AwKkVwjVmFQ3bcO7j0McGYAqCKH2a326fswfofng/E8VewCT/raeeGQr4huVhY704deK8AWASSTlxzMj0eZc6Q=="], + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.64.0", "", { "os": "linux", "cpu": "none" }, "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g=="], - "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-xsRpTxfUnJF8D3AUKko/qyWdjw4GZVHlCVFuGlzSCTeewLmykKINW8em1+wx+axsDVtJJcMtvsiaXggXxrlHgw=="], + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.64.0", "", { "os": "linux", "cpu": "none" }, "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw=="], - "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-Z4AYOTcy7nYEIiXwD62PlerimyYRcfJOgUbQAEBjXz098kxKuERBlRntofGy69HHhe9E0TLVNMl1yspVNu+efw=="], + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.64.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ=="], - "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.58.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-A3nhhtZPC/TKVWOPj9q/H3p2znJDCcHWYlJBhWL8hGq/bFmBaNBHC8Np6E581yVq1w9Mi3rMDNzDalWvtUfJtQ=="], + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.64.0", "", { "os": "linux", "cpu": "x64" }, "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog=="], - "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2g+tVkgwqphw8R4hgo+kF4oz8+P5RwVOtr9+irsC7uwEp0e9j7Crw8kDGKL20uYlLPD7g02DqA61mC/UNYx98A=="], + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.64.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg=="], - "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rc15P6AbyyB7426aN8AakLd02Trb3a6ML/mmfAQeVHJEfVofWLcWIrBdy6zDEY+DIaL/s8E4GGPboVw+oP3+EA=="], + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.64.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw=="], - "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.58.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ZWoTM27/HYPOh9iq86DAbhPu9nXb8qKvvGU/h8OfliyVUFAMMNTLDkGsWDKKnDqIkqvZ9+dXlgUOsH1LYO3O7g=="], + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.64.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ=="], - "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.58.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-LHZnqFXe2dEfkRI4XdZS/57nEOT/I4UCRX5IyM9v4GYW9XwQCjGe1IUK59SuKw3POwvcgWQ4pme2cYXmNqTNPg=="], + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.64.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ=="], - "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.58.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-mZKpg20TpheCJym1rarcZCUJeW1sSruw8zAAaCYWvuVfwIUDN1CXdrPU/JgCWReXTCTrEfCB8Wyo3hh9jSZ2EA=="], + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.64.0", "", { "os": "win32", "cpu": "x64" }, "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g=="], - "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-N/wUU4N5PZ2orBtI+Ko7MnMfYLfE7K91UrGMY/c/pYyHR3lA9kwst1XugkZx+92YcRh/Eo+iv2eTESSWXfiZPA=="], + "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2001", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w=="], - "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C2uMmwK5Bc4ri4ysZ6sA8Rcu+A5zBQTp6ml2u0CLLbRZp4kMFPV3yWk8B5DK9Aw7y9bbjogIm75tUwGLFzlsYQ=="], + "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@7.0.2001", "", { "os": "darwin", "cpu": "x64" }, "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw=="], - "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Wgvt/1lRbDxmoNqWQKKcL+UIiqLmdJ+EWLpQa1qzoNVAfNB0PJpa82/8dH1twT/3rSs4zrP5TXPWl4juB71WuQ=="], + "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@7.0.2001", "", { "os": "linux", "cpu": "arm64" }, "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A=="], - "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PB1rxII7KV83+ASY4sSkXtqvpij6ME66+QCRL49uksi/ofs2Rf/UVboYr095n0Rkbl2wgvlsHGl6DHC361jQUQ=="], + "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@7.0.2001", "", { "os": "linux", "cpu": "x64" }, "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ=="], - "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-xcz3CxKmjTQLREtE/UShh+ruWmm9nAb7UM9zKcD65BStiuYgOakAKkPHl4YS5DztpVcDrE0+HqbOolTlRKYWmw=="], + "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@7.0.2001", "", { "os": "win32", "cpu": "arm64" }, "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q=="], - "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-A2i6ZGBec3i20S7RaxkgHc6r3HYtD5Mn7j/mb22NkTz14u0JuudvTu6JggAnbGMcv8+dBKQI//EasxSPJLD8pw=="], + "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2001", "", { "os": "win32", "cpu": "x64" }, "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog=="], - "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0ZbGd9qRB6zs82moekaKdEvncRANq49EAwfNX62JpTS46feXUhKAuoyVDvZMj6Rywejylrmmu79Wo6faYCo4Ew=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.79.0", "", { "os": "android", "cpu": "arm" }, "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.73.0", "", { "os": "android", "cpu": "arm" }, "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg=="], + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.79.0", "", { "os": "android", "cpu": "arm64" }, "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg=="], - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.73.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ=="], + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.79.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag=="], - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.73.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA=="], + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.79.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA=="], - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.73.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg=="], + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.79.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg=="], - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.73.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg=="], + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA=="], - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.73.0", "", { "os": "linux", "cpu": "arm" }, "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w=="], + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.79.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A=="], - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.73.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg=="], + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g=="], - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.73.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw=="], + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.79.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw=="], - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.73.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw=="], + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.79.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g=="], - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.73.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q=="], + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg=="], - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.73.0", "", { "os": "linux", "cpu": "none" }, "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA=="], + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.79.0", "", { "os": "linux", "cpu": "none" }, "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg=="], - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.73.0", "", { "os": "linux", "cpu": "none" }, "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A=="], + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.79.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg=="], - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.73.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ=="], + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ=="], - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.73.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw=="], + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.79.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg=="], - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.73.0", "", { "os": "linux", "cpu": "x64" }, "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q=="], + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.79.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q=="], - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.73.0", "", { "os": "none", "cpu": "arm64" }, "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw=="], + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.79.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw=="], - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.73.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w=="], + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.79.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog=="], - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.73.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg=="], + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.79.0", "", { "os": "win32", "cpu": "x64" }, "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.73.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw=="], - - "@oxlint/plugins": ["@oxlint/plugins@1.73.0", "", {}, "sha512-OhgMQeMmZA0dcFcX4/priaJZWdFECxiClgq6mRX6aatZEcV9PbKC3P3/v8U1hVjviT1i5U+vR8lAtBV6m4FXAA=="], + "@oxlint/plugins": ["@oxlint/plugins@1.79.0", "", {}, "sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.6", "", { "os": "android", "cpu": "arm64" }, "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.6", "", { "os": "linux", "cpu": "arm" }, "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.6", "", { "os": "none", "cpu": "arm64" }, "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], @@ -233,13 +223,11 @@ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], - "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + "@testing-library/user-event": ["@testing-library/user-event@14.6.6", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -247,43 +235,83 @@ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], + "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], - "@vitest/browser": ["@vitest/browser@4.1.10", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.10" } }, "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng=="], + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], - "@vitest/browser-preview": ["@vitest/browser-preview@4.1.10", "", { "dependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/user-event": "^14.6.1", "@vitest/browser": "4.1.10" }, "peerDependencies": { "vitest": "4.1.10" } }, "sha512-14MJrL59ZFkqXLjwfSk6RzTDy5Czf9UG4+8q8L6Gxjs2aPjEce/cVNYV14bXAc2BvMjUNu904+ZEZA1Xc1wtvQ=="], + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], - "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], - "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], - "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], - "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], - "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], - "@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.2.5", "", { "dependencies": { "@oxc-project/runtime": "=0.139.0", "@oxc-project/types": "=0.139.0", "lightningcss": "^1.32.0", "postcss": "^8.5.6", "yuku-codegen": "^0.5.44", "yuku-parser": "^0.5.44" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-fxMGImIOyOipwCX6udOD1S9Q1xXfaimv6kcRgLWBxLsy7oryAyXqVfoYr7bmmAdSDlIutHRgvA6eiqfJjARTHA=="], + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], - "@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M62R3gmoHZbhL+UHHTevJi9a3aJyY+Eid8GAOtxEsRMkHmJ8IwOSOBERXM3C4CULvEa/ORYKiUQnqo5ewF44Fw=="], + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], - "@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-a1h1dv/7QcnlqlN6yZBIPjgHSxbeyY/IcTxepTAOpPB7eAi1RPb1+cCkwo7c3MnDPJb3iZzwD0rm2+fOUoZp0w=="], + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], - "@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-t8bS8fA2a3OSAEaHdgJFmzd0TkWh9yAxIoKAprsOleIcUEmzDxhH8drTj9TPyTrChKpv0aJTsK5ZK3RzcCUkdg=="], + "@vitest/browser": ["@vitest/browser@4.1.11", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.11" } }, "sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w=="], - "@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-vnsjQI3LEUYFMR3LCMqtAxaZav8BNypSAf8YzcFu9+Qtd1dcCrAUz9RrEBCIIqEiw0p0O+SrX2CcMHSSnzWbKA=="], + "@vitest/browser-preview": ["@vitest/browser-preview@4.1.11", "", { "dependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/user-event": "^14.6.1", "@vitest/browser": "4.1.11" }, "peerDependencies": { "vitest": "4.1.11" } }, "sha512-iPKSE6Ibayey6HFgK1V1/aHgyhx7HSRk1YMi+lnBZGmlIiNV5Uc7xRkD9Su8RDylTxDECK23t7kTHdRKoqSYDQ=="], - "@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-3xlXrxIz8UKGcGefifkhoMpsTIMdgqikwQuDUqgG5O7/b2tetpK9aoT4C9b2fQGkhYUpCJwdD83AtywQ4EhoWA=="], + "@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="], - "@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-XylGiayBoD7vt1/SKfmh5FoBNdKI5EWlIb5Sd9A1oTQW8DLi97VcEgVZ7vh/8kG1OEe+9z1lyRis6RDql2KDUw=="], + "@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="], - "@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-eN1zvUAqXVXOC72WOVu9gz/jxr9tBrga/5lCsDjHeZDJ/bzhDVJ4eYZUDZzasPE1QCbAhmuIJsv0WzEUxdGAzA=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="], - "@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-jgXVYK8crlR5cQ07vX5Qw2K+boNLKMxarPgE3/AyPPRUtGC8iCpZz0RCPoJHDmTh7848Ra5Fnt4LEfbcUv1ByQ=="], + "@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="], + + "@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="], + + "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], + + "@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.3.0", "", { "dependencies": { "@oxc-project/runtime": "=0.146.0", "@oxc-project/types": "=0.146.0", "lightningcss": "^1.33.0", "postcss": "^8.5.6", "yuku-codegen": "^0.5.44", "yuku-parser": "^0.5.44" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.0", "@voidzero-dev/vite-plus-darwin-x64": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.0", "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-aOqoqIWaF+Q/geDU48pC2rVFEVSvLV1GGj/NdvhUiBhCZntoFNbwI+hjUeG8BMaPG67sOV6ey+/sgkdmGmKqaw=="], + + "@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9ADr1egZ8T4tJOqrpQLhoDl95Y74R95+bsvjmin0gy1C0eQVhpmcNnBfb07KFNhJioJp9MMO7F7Dx4fQL5SKsw=="], + + "@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-GegasVCwNeDOkNyvhLOuwU1+T2JkjY/Tq+SOvwphUpVcqQ6OOAUq9LlpoXviO2QL/Kq2NbMYjiAfPKVSTLUFQw=="], + + "@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-nYI3KNYXkXjRPsSdR4Lr7J2xMxfR1+TplWlG/dV37qVXWAjbyHpoAlbULjZBAVJMyXRNlcADhBrEwXe4g6s48A=="], + + "@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HRlVA3AOcuGXmOdHhQ+Zv5XAaKbYF9si5rRHoOsKl0UyBo4txA3OoJfmP0WjanfLUNmu85JyO2dO1ptL4C6wgg=="], + + "@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9A+dFScPfwcrzF/rRR0zH8++2hOf6xtFmN/5LyzyfUywtw9MILXcC72IMcOeL6QRJwKUMsudi1rFeDE59azNvw=="], + + "@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-KfIV3qaPdaOOE8JQMRHRE34FtZocl9O86XLTP6JMjDUlcx8FPgf8/fz/HFqJ8g232vM+JsgLI/YTVeXP8LkTKw=="], + + "@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-KRhdy5K13AYx9KBfCVHRrK7zSZU+bMW9CL6gTai+UkJgAmDJi1kjdSNboZOjO8mrzUnTCrELgMI2tnstcxSTuA=="], + + "@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7+G+GxGmxdpQO0zjiGnkZFXKGqm0CrVduebRsJd6ccuOuxCQYPxLcoHq4WOaGrh56SrAGS7XjhnQCrXRkzKUVQ=="], "@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.5.48", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yo96Oef12WzqnphInfz/eexVse3+kWgfGS5g2S3rFS3dcGn1ENW9xLFDZUP9rh+yP76DOq38wBoFi1+I9+6qBg=="], @@ -347,7 +375,7 @@ "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -391,7 +419,7 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], @@ -403,19 +431,19 @@ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], + "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], - "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + "fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -441,15 +469,15 @@ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - "hono": ["hono@4.12.25", "", {}, "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="], + "hono": ["hono@4.13.5", "", {}, "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -457,7 +485,7 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -467,29 +495,29 @@ "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -497,7 +525,7 @@ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], @@ -509,51 +537,51 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "opencode-ai": ["opencode-ai@1.18.4", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.18.4", "opencode-darwin-x64": "1.18.4", "opencode-darwin-x64-baseline": "1.18.4", "opencode-linux-arm64": "1.18.4", "opencode-linux-arm64-musl": "1.18.4", "opencode-linux-x64": "1.18.4", "opencode-linux-x64-baseline": "1.18.4", "opencode-linux-x64-baseline-musl": "1.18.4", "opencode-linux-x64-musl": "1.18.4", "opencode-windows-arm64": "1.18.4", "opencode-windows-x64": "1.18.4", "opencode-windows-x64-baseline": "1.18.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-B8pFAs1g158ZU+C6eSiJz/5ZhG7Vr2mH7Z7ruYEHLElAlX9tehrRTnzTTgjSxHY2vwZ/5VplwR3odFU+Dai8jg=="], + "opencode-ai": ["opencode-ai@1.18.23", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.18.23", "opencode-darwin-x64": "1.18.23", "opencode-darwin-x64-baseline": "1.18.23", "opencode-linux-arm64": "1.18.23", "opencode-linux-arm64-musl": "1.18.23", "opencode-linux-x64": "1.18.23", "opencode-linux-x64-baseline": "1.18.23", "opencode-linux-x64-baseline-musl": "1.18.23", "opencode-linux-x64-musl": "1.18.23", "opencode-windows-arm64": "1.18.23", "opencode-windows-x64": "1.18.23", "opencode-windows-x64-baseline": "1.18.23" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-3NkT0XINL7d0HYkTyGV1SPChHXhvRgKqNaTgKRTGb0TXUWszXA7MW/y3zMZw29y1AQuUDAzRvVYmQ9KGRQhroA=="], - "opencode-darwin-arm64": ["opencode-darwin-arm64@1.18.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/GdcUv3axBFYmpdCY3yMGRCBq3fwAJayflnn9stXtIHKuckc7ZaUYZmyudIUpmzkZP5W8XsAVMJomR4Rbr2+Ig=="], + "opencode-darwin-arm64": ["opencode-darwin-arm64@1.18.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QP9PjwpHtZoLVXw2WvUmPZecz7mWbQkT4t3K36B//fCaDG+zWa+SsztIeaW5azujNwwtUemLA5icE/zINng48Q=="], - "opencode-darwin-x64": ["opencode-darwin-x64@1.18.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-0i/AmdFw3CerwIggKqtaDlWDCBfSgzXT+KPSvks/Dx6l9NfYHihLnbV+vKbr0lwzco1lsDo4JHE4j/zz7JZUKQ=="], + "opencode-darwin-x64": ["opencode-darwin-x64@1.18.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-R9nWP3edz/0FnEfwmuxtiWBB7bS4NtZCyCffJyiMlrbwdDC+bIXYrWxWXVrzaP1mJujs6g2MAwTCUSx/qpBhDw=="], - "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.18.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1v/keJ/m+3UplGU2XdP5YfK25rTlGi7Kf5BVSV/ICObk6PiJWBAWOrB8bAQWDLhN/Tn7UfogBAgxaiRRfLPuEA=="], + "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.18.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-QGx6I/nFYur7qJ/Nx2L3fC4XYQt44cyDsm7p8twNA+cdjGX3ndnPbMdAl5ikdZyAfSMUGYK8VWY2JMxv0rmfjw=="], - "opencode-linux-arm64": ["opencode-linux-arm64@1.18.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-3gBw9weE76jloNPGmPvzl3GJR8b6scwSAENxUKRdJ3WCkC7qcjJm49KFyfqFjO5++1H1Q2jbmqqxAgt5upqoMQ=="], + "opencode-linux-arm64": ["opencode-linux-arm64@1.18.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-g1zDFhuE9FOYwjSGderlu69wfd4GQzS0xsDiIY11QUciuBmM6DrHqLvmhlLFsUVHYVnCPY1YeN1pq5ewE3x72Q=="], - "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.18.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-kno1QJz+JoY1YqGlrxcwGKqOh2/OFKdQ4UGUCN5K2evQRIhd9EfB4JvZkjzLxTzglda8lpP+PJltjCA4DHJAJw=="], + "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.18.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-VyDkzUJfJgkx9h9RhazTW9xeTgSXBmVFPblsbPGBW9tR612f6gjQxfOfu4cpHnQHK1qjuW9ClzLKHWqv3EcJTA=="], - "opencode-linux-x64": ["opencode-linux-x64@1.18.4", "", { "os": "linux", "cpu": "x64" }, "sha512-mCiZuKQBqRHVhtp20YFDfNf5cUwTt6/I98MFJQDJdDTWggnkAVOnitQTmSt2cQwoPJ94VrRwDhZyY3pIY3+CQg=="], + "opencode-linux-x64": ["opencode-linux-x64@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-5x9d1Cm/YtqzR6lAlNbgVprTQ3R3hx7qGTWCzm5l5u6lBNkhYTrhy2s8k25dxKKuxqZ9Kngqz9JYWvSVHy2Lmw=="], - "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.18.4", "", { "os": "linux", "cpu": "x64" }, "sha512-LlVIv54gpM8I6QJVAoW5uvcaVO4z+y5DswfUCsxgaD0CWgjEbULaiAAZIS/L+N5rRw/jHeqcHSXiKinFZt0xww=="], + "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-yUhBOXfTQour2JCdAkwD3DDqSnyxB0grefwdPqEhYmJHIkYxfJIIzyy6V//pyouvkE0XMouFtiuZXw8S6Wo0iQ=="], - "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.18.4", "", { "os": "linux", "cpu": "x64" }, "sha512-hM/uPMEuxLbwXueOmHsf3jjmfJ238/3r6FZMPumKVMR7jJHutp3ZWuOIL+ZzByxzsUK1f3RGbKaFULOMIieRSw=="], + "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-c1DPxauhzAurlIBhJBr/rokDpc65l084T4qTl36gDDT9Xzc/Nk5Q5yMDaPm1DDI3WeHKDt11MDlxT5AjQW5gtw=="], - "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.18.4", "", { "os": "linux", "cpu": "x64" }, "sha512-AlojHLyv7Cgn2g5Rj5QeHckEWyA20qdbEla2d1R9mXSaqlP6sX7izNti8Tzr/vMG53Uwmppyjb37uFB2fK+YTQ=="], + "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-t/5mlnTBZKdZpqKHwdwxlWqGakntauvMSmXtyJc17M7XJRmZaaGHtNSaSefbbYFIL4agoQCXTvIkhhyxOvr7zQ=="], - "opencode-windows-arm64": ["opencode-windows-arm64@1.18.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-B6m7N7ZPj/E6xx1ZaSjNW0gHTd0b3NTSf9mQiN3W89zji3oH9VOHLQdrP10rAd/2uZ5ZttxSkRqj/iwgAW+3TQ=="], + "opencode-windows-arm64": ["opencode-windows-arm64@1.18.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-QtJQcLU0yPz6on3jjks3f/EHgZuIDFw7FvAKu3wsHhL09NYDh7GczfRXDPRHa3NgqnU8dkB8p9mhqgaPRPogoQ=="], - "opencode-windows-x64": ["opencode-windows-x64@1.18.4", "", { "os": "win32", "cpu": "x64" }, "sha512-TumOcMOvZ6vfs348LgGLK9eFEBLsh+wI/UXmQ8BhXW95YV1Wd26TutSzQi4L/wLftsDfl8Q6bKQ2trWtGBzBvQ=="], + "opencode-windows-x64": ["opencode-windows-x64@1.18.23", "", { "os": "win32", "cpu": "x64" }, "sha512-mMaIITuXzkNfjdcYL8uZaZuMDjulFyH/UCq9bxblam2mUZf9uWisoi5J6CXFsS/mkN7CZfTAt6PttSp4n3PH4g=="], - "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.18.4", "", { "os": "win32", "cpu": "x64" }, "sha512-4EpZ97uI50vVv9UT5sDeK3ff+sFzqmbBT4TNGkMF9uwNTKJYqoSVuOIZhIfLTLNlrEK2hd2/sf/4GQhIkM6iTw=="], + "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.18.23", "", { "os": "win32", "cpu": "x64" }, "sha512-AqXsTKaPcDx3rrid5bLUwJbQ/3vr9rJ6fvOStIznTzwrbOgP8wy5G4jCoIzu6KB/WxGx/d1MrV4cGaJ73qnjBA=="], - "oxfmt": ["oxfmt@0.58.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.58.0", "@oxfmt/binding-android-arm64": "0.58.0", "@oxfmt/binding-darwin-arm64": "0.58.0", "@oxfmt/binding-darwin-x64": "0.58.0", "@oxfmt/binding-freebsd-x64": "0.58.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.58.0", "@oxfmt/binding-linux-arm-musleabihf": "0.58.0", "@oxfmt/binding-linux-arm64-gnu": "0.58.0", "@oxfmt/binding-linux-arm64-musl": "0.58.0", "@oxfmt/binding-linux-ppc64-gnu": "0.58.0", "@oxfmt/binding-linux-riscv64-gnu": "0.58.0", "@oxfmt/binding-linux-riscv64-musl": "0.58.0", "@oxfmt/binding-linux-s390x-gnu": "0.58.0", "@oxfmt/binding-linux-x64-gnu": "0.58.0", "@oxfmt/binding-linux-x64-musl": "0.58.0", "@oxfmt/binding-openharmony-arm64": "0.58.0", "@oxfmt/binding-win32-arm64-msvc": "0.58.0", "@oxfmt/binding-win32-ia32-msvc": "0.58.0", "@oxfmt/binding-win32-x64-msvc": "0.58.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-8feG/7NVEHDVwc1OUpP6Pks+TnaDFUw2jLLFIMi5bcmmwxAX2wBQvjSzj62RRTYBf2Op1Wt8xbkmagmPTR5ETg=="], + "oxfmt": ["oxfmt@0.64.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.64.0", "@oxfmt/binding-android-arm64": "0.64.0", "@oxfmt/binding-darwin-arm64": "0.64.0", "@oxfmt/binding-darwin-x64": "0.64.0", "@oxfmt/binding-freebsd-x64": "0.64.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", "@oxfmt/binding-linux-arm64-gnu": "0.64.0", "@oxfmt/binding-linux-arm64-musl": "0.64.0", "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-musl": "0.64.0", "@oxfmt/binding-linux-s390x-gnu": "0.64.0", "@oxfmt/binding-linux-x64-gnu": "0.64.0", "@oxfmt/binding-linux-x64-musl": "0.64.0", "@oxfmt/binding-openharmony-arm64": "0.64.0", "@oxfmt/binding-win32-arm64-msvc": "0.64.0", "@oxfmt/binding-win32-ia32-msvc": "0.64.0", "@oxfmt/binding-win32-x64-msvc": "0.64.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg=="], - "oxlint": ["oxlint@1.73.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.73.0", "@oxlint/binding-android-arm64": "1.73.0", "@oxlint/binding-darwin-arm64": "1.73.0", "@oxlint/binding-darwin-x64": "1.73.0", "@oxlint/binding-freebsd-x64": "1.73.0", "@oxlint/binding-linux-arm-gnueabihf": "1.73.0", "@oxlint/binding-linux-arm-musleabihf": "1.73.0", "@oxlint/binding-linux-arm64-gnu": "1.73.0", "@oxlint/binding-linux-arm64-musl": "1.73.0", "@oxlint/binding-linux-ppc64-gnu": "1.73.0", "@oxlint/binding-linux-riscv64-gnu": "1.73.0", "@oxlint/binding-linux-riscv64-musl": "1.73.0", "@oxlint/binding-linux-s390x-gnu": "1.73.0", "@oxlint/binding-linux-x64-gnu": "1.73.0", "@oxlint/binding-linux-x64-musl": "1.73.0", "@oxlint/binding-openharmony-arm64": "1.73.0", "@oxlint/binding-win32-arm64-msvc": "1.73.0", "@oxlint/binding-win32-ia32-msvc": "1.73.0", "@oxlint/binding-win32-x64-msvc": "1.73.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.24.0", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ=="], + "oxlint": ["oxlint@1.79.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.79.0", "@oxlint/binding-android-arm64": "1.79.0", "@oxlint/binding-darwin-arm64": "1.79.0", "@oxlint/binding-darwin-x64": "1.79.0", "@oxlint/binding-freebsd-x64": "1.79.0", "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", "@oxlint/binding-linux-arm-musleabihf": "1.79.0", "@oxlint/binding-linux-arm64-gnu": "1.79.0", "@oxlint/binding-linux-arm64-musl": "1.79.0", "@oxlint/binding-linux-ppc64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-gnu": "1.79.0", "@oxlint/binding-linux-riscv64-musl": "1.79.0", "@oxlint/binding-linux-s390x-gnu": "1.79.0", "@oxlint/binding-linux-x64-gnu": "1.79.0", "@oxlint/binding-linux-x64-musl": "1.79.0", "@oxlint/binding-openharmony-arm64": "1.79.0", "@oxlint/binding-win32-arm64-msvc": "1.79.0", "@oxlint/binding-win32-ia32-msvc": "1.79.0", "@oxlint/binding-win32-x64-msvc": "1.79.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg=="], - "oxlint-tsgolint": ["oxlint-tsgolint@0.24.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.24.0", "@oxlint-tsgolint/darwin-x64": "0.24.0", "@oxlint-tsgolint/linux-arm64": "0.24.0", "@oxlint-tsgolint/linux-x64": "0.24.0", "@oxlint-tsgolint/win32-arm64": "0.24.0", "@oxlint-tsgolint/win32-x64": "0.24.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-giCk5sEvG02d5tzPmFMX3hem8ndzEEu1xvGYS5OwNfO2WGl6ZVxt5LjE0yiMDoz94INI7XkXwgFAQiydPvVHDw=="], + "oxlint-tsgolint": ["oxlint-tsgolint@7.0.2001", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2001", "@oxlint-tsgolint/darwin-x64": "7.0.2001", "@oxlint-tsgolint/linux-arm64": "7.0.2001", "@oxlint-tsgolint/linux-x64": "7.0.2001", "@oxlint-tsgolint/win32-arm64": "7.0.2001", "@oxlint-tsgolint/win32-x64": "7.0.2001" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -565,23 +593,23 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], - "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], "radash": ["radash@12.1.1", "", {}, "sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], @@ -589,7 +617,7 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], + "rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -625,19 +653,19 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -645,27 +673,25 @@ "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="], + "type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], - "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vestig": ["vestig@0.23.0", "", {}, "sha512-Jo9HAym5MyXn3zY4L1T/e7zGwP3Okf5VWtSWeSvjpeAbDD6iparFRxrlRSpwZLCxJY1DSlBFiHRLX4+wYVNtiA=="], + "vestig": ["vestig@0.24.1", "", {}, "sha512-sQTGvU3VdPgtM3PE3Eweo7AqJJPeoDl3Wke2OEVJRA5lZclZ5CmgDHLKCfvOJ/S82SyXtyzwCeQNvTxSbPttWg=="], - "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="], + "vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="], - "vite-plus": ["vite-plus@0.2.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@oxlint/plugins": "=1.73.0", "@vitest/browser": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "@voidzero-dev/vite-plus-core": "0.2.5", "oxfmt": "=0.58.0", "oxlint": "=1.73.0", "oxlint-tsgolint": "=0.24.0", "vitest": "4.1.10" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.2.5", "@voidzero-dev/vite-plus-darwin-x64": "0.2.5", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.2.5", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.2.5", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.2.5", "@voidzero-dev/vite-plus-linux-x64-musl": "0.2.5", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.2.5", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.2.5" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.10", "@vitest/browser-webdriverio": "4.1.10" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio"], "bin": { "oxfmt": "bin/oxfmt", "oxlint": "bin/oxlint", "vp": "bin/vp", "vpr": "bin/vpr" } }, "sha512-QNJ0FnN8rfs5u8lZKZ1uR2Tegjg3VkT0AGTxSLGHg4fYmGxRNfAV0YX1parZrVa4VybSI56SWCoo7wQ6D7pMew=="], + "vite-plus": ["vite-plus@0.3.0", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@oxlint/plugins": "=1.79.0", "@vitest/browser": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "@voidzero-dev/vite-plus-core": "0.3.0", "oxfmt": "=0.64.0", "oxlint": "=1.79.0", "oxlint-tsgolint": "=7.0.2001", "vitest": "4.1.11" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.0", "@voidzero-dev/vite-plus-darwin-x64": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.0", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.0" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.11", "@vitest/browser-webdriverio": "4.1.11" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio"], "bin": { "oxfmt": "./bin/oxfmt", "oxlint": "./bin/oxlint", "vp": "./bin/vp", "vpr": "./bin/vpr" } }, "sha512-GNWbWuWD37frCSFrz6MLzUo62bTv5IOJozHEgZYOkxsLkuQtTwm4TowzpfoGrSsfwhAAtfPd/sK1Y0+v1SwhZA=="], - "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], + "vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -673,7 +699,7 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "yuku-codegen": ["yuku-codegen@0.5.48", "", { "dependencies": { "@yuku-toolchain/types": "0.5.43" }, "optionalDependencies": { "@yuku-codegen/binding-darwin-arm64": "0.5.48", "@yuku-codegen/binding-darwin-x64": "0.5.48", "@yuku-codegen/binding-freebsd-x64": "0.5.48", "@yuku-codegen/binding-linux-arm-gnu": "0.5.48", "@yuku-codegen/binding-linux-arm-musl": "0.5.48", "@yuku-codegen/binding-linux-arm64-gnu": "0.5.48", "@yuku-codegen/binding-linux-arm64-musl": "0.5.48", "@yuku-codegen/binding-linux-x64-gnu": "0.5.48", "@yuku-codegen/binding-linux-x64-musl": "0.5.48", "@yuku-codegen/binding-win32-arm64": "0.5.48", "@yuku-codegen/binding-win32-x64": "0.5.48" } }, "sha512-p7HxD5Xl4jzDzqMrGePAOeSHmRY4g58h4HuGq15weQFPxuPWd/W6e7nqp/+Lea6JfpOdBwJOAyXFqIZ/J9Zfnw=="], @@ -683,12 +709,12 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="], - "vite/postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], + "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], } } diff --git a/package.json b/package.json index 004c9b1..b8c9c8a 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "files": [ "dist", "src", + "!src/runtimes/openai/generated", "assets" ], "type": "module", @@ -83,31 +84,31 @@ "bench": "bun bench/ttft-bench.ts", "build": "vp run build:types && bun build src/bin/driver.ts --target bun --outfile dist/driver.mjs", "build:types": "rm -rf dist/types && vp exec tsc -p tsconfig.types.json", - "build:image": "vp run build && buildah build -t agent-driver:local .", - "docker:smoke:environment": "docker run --rm --entrypoint node agent-driver:local /usr/local/libexec/mosoo/environment-package-manager-check.mjs smoke", + "build:image": "vp run build && buildah build --http-proxy=false --platform linux/amd64 -t agent-driver:local .", + "test:image:environment": "podman run --pull=never --rm --entrypoint node agent-driver:local /usr/local/libexec/mosoo/environment-package-manager-check.mjs smoke", "check": "vp check && vp run tc && vp run test && vp run build", "prepack": "vp run build" }, "dependencies": { - "@agentclientprotocol/sdk": "1.2.1", - "@anthropic-ai/claude-agent-sdk": "0.3.211", - "@anthropic-ai/sdk": "0.111.0", - "@modelcontextprotocol/client": "^2.0.0-alpha.2", - "@orpc/client": "^1.14.3", + "@agentclientprotocol/sdk": "1.4.0", + "@anthropic-ai/claude-agent-sdk": "0.3.247", + "@anthropic-ai/sdk": "0.121.0", + "@modelcontextprotocol/client": "^2.0.0", + "@orpc/client": "^1.15.0", "fflate": "^0.8.3", - "vestig": "^0.23.0", + "vestig": "^0.24.1", "zod": "^4.4.3" }, "devDependencies": { - "@openai/codex-sdk": "0.144.5", - "@types/bun": "1.3.14", - "@types/node": "^25.8.0", - "opencode-ai": "1.18.4", - "typescript": "^6.0.3", - "vite-plus": "0.2.5" + "@openai/codex": "0.150.1", + "@types/bun": "1.4.0", + "@types/node": "^26.3.0", + "opencode-ai": "1.18.23", + "typescript": "^7.0.2", + "vite-plus": "0.3.0" }, "engines": { - "bun": ">=1.3.14" + "bun": ">=1.4.0" }, - "packageManager": "bun@1.3.14" + "packageManager": "bun@1.4.0" } diff --git a/src/bin/driver-process.ts b/src/bin/driver-process.ts index c67252e..8c914d0 100644 --- a/src/bin/driver-process.ts +++ b/src/bin/driver-process.ts @@ -24,7 +24,7 @@ import { DRIVER_PROTOCOL_VERSION } from "../protocol/boot"; import type { DriverBootPayload } from "../protocol/boot"; import { createDriverHostIntegrationSnapshotFromBootExecution } from "../protocol/host-integration"; import type { DriverHostIntegrationSnapshot } from "../protocol/host-integration"; -import { parseDriverId } from "../protocol/id"; +import { parseRunId } from "../protocol/id"; import type { RunId } from "../protocol/id"; import { createDriverStartInputFromBootPayload } from "../protocol/start"; import type { DriverStartInput } from "../protocol/start"; @@ -41,7 +41,7 @@ const DRIVER_BACKEND_START_TIMEOUT_MS = 60_000; const DRIVER_SHUTDOWN_TIMEOUT_MS = 5_000; function parseNullableRunId(value: string | null): RunId | null { - return value === null ? null : (parseDriverId(value, "Run ID") as RunId); + return value === null ? null : parseRunId(value); } export class DriverProcess { diff --git a/src/contract/reducer.ts b/src/contract/reducer.ts index a95776f..30ae46d 100644 --- a/src/contract/reducer.ts +++ b/src/contract/reducer.ts @@ -1,6 +1,6 @@ import type { CommittedMutation } from "./mutation"; import { committedMutationSchema } from "./mutation"; -import { assertProtocolAdmission, compareTimestamps } from "./common"; +import { PROTOCOL_VERSION, assertProtocolAdmission, compareTimestamps } from "./common"; import type { SyncPayload } from "./sync"; import { syncPayloadSchema } from "./sync"; import type { SessionSnapshot } from "./state"; @@ -169,7 +169,7 @@ function applyMutation(current: SessionSnapshot, mutation: CommittedMutation): S } const next: SessionSnapshot = { - protocolVersion: 2, + protocolVersion: PROTOCOL_VERSION, revision: mutation.revision, capturedAt: mutation.committedAt, session, diff --git a/src/contract/state-validation.ts b/src/contract/state-validation.ts index 0720e6e..2699212 100644 --- a/src/contract/state-validation.ts +++ b/src/contract/state-validation.ts @@ -135,7 +135,14 @@ function assertRunTransition(previous: Run | undefined, next: Run): void { "Run origin fields cannot change.", ); - for (const key of ["cachedInput", "input", "output", "reasoning", "total"] as const) { + for (const key of [ + "cachedInput", + "cachedWrite", + "input", + "output", + "reasoning", + "total", + ] as const) { const prior = previous.usage?.[key]; if (prior === undefined) { diff --git a/src/contract/state.ts b/src/contract/state.ts index 4cd3b3f..a3a0469 100644 --- a/src/contract/state.ts +++ b/src/contract/state.ts @@ -79,6 +79,7 @@ export const tokenUsageSchema = z.strictObject({ input: z.number().int().nonnegative().safe().optional(), output: z.number().int().nonnegative().safe().optional(), cachedInput: z.number().int().nonnegative().safe().optional(), + cachedWrite: z.number().int().nonnegative().safe().optional(), reasoning: z.number().int().nonnegative().safe().optional(), total: z.number().int().nonnegative().safe().optional(), cost: z @@ -291,7 +292,7 @@ export const permissionOptionSchema = z.strictObject({ label: z.string().min(1), description: z.string().optional(), effect: z.enum(["allow", "deny"]), - scope: z.enum(["once", "session"]), + scope: z.enum(["once", "session", "persistent"]), extensions: extensionsSchema.optional(), }); diff --git a/src/core/agent-driver-backend.ts b/src/core/agent-driver-backend.ts index f6fee50..70a08f8 100644 --- a/src/core/agent-driver-backend.ts +++ b/src/core/agent-driver-backend.ts @@ -65,8 +65,6 @@ function toAgentDriverEventSink( return eventSink; } - const currentRunId = eventSink.currentRunId?.bind(eventSink); - return { claimExternalToolEffect: async () => { throw new Error("Driver external tool effect ledger is not configured."); @@ -75,7 +73,7 @@ function toAgentDriverEventSink( completeExternalToolEffect: async () => { throw new Error("Driver external tool effect ledger is not configured."); }, - ...(currentRunId === undefined ? {} : { currentRunId }), + currentRunId: eventSink.currentRunId.bind(eventSink), markExternalToolEffectUnknown: async () => {}, pushEvents: (input) => eventSink.pushEvents(input), }; diff --git a/src/core/driver-command-dispatcher.ts b/src/core/driver-command-dispatcher.ts index ab93d89..132b627 100644 --- a/src/core/driver-command-dispatcher.ts +++ b/src/core/driver-command-dispatcher.ts @@ -1,7 +1,7 @@ import { createScopedWideEvent, emitWideEvent } from "../observability"; import type { Logger } from "../observability"; import { summarizeRuntimeCommand } from "../observability/driver-debug"; -import { parseDriverId } from "../protocol/id"; +import { parseRunId } from "../protocol/id"; import type { RunId } from "../protocol/id"; import type { McpExecuteCommandResult, RunError, RuntimeCommand } from "../runtime-command"; import { promiseWithTimeout, raceWithAbort, sleepPromise } from "../utils/async"; @@ -12,8 +12,11 @@ import { PermissionEventDeliveryError, type DriverPermissionBroker, } from "./driver-permission-broker"; -import { pushLosslessEvents } from "./driver-runtime-io"; -import type { DriverRuntimeIo } from "./driver-runtime-io"; +import { + DRIVER_EVENT_DELIVERY_TIMEOUT_MS, + pushLosslessEvents, + type DriverRuntimeIo, +} from "./driver-runtime-io"; import type { DriverRuntimeStateMachine } from "./driver-runtime-state"; import { DriverTurnCancellationCleanupError, @@ -35,8 +38,8 @@ interface DriverCommandDispatcherOptions { } const COMMAND_POLL_INTERVAL_MS = 250; -const ACTIVE_INPUT_SETTLE_GRACE_MS = 5_000; export const ACTIVE_TURN_CANCEL_GRACE_MS = 2_000; +const ACTIVE_INPUT_SETTLE_GRACE_MS = ACTIVE_TURN_CANCEL_GRACE_MS + DRIVER_EVENT_DELIVERY_TIMEOUT_MS; const EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS = 2_000; const MAX_ACTIVE_MCP_COMMANDS = 32; @@ -74,10 +77,6 @@ class ExternalToolEffectResolutionRequiredError extends Error { } } -function parseRunId(value: string): RunId { - return parseDriverId(value, "Run ID") as RunId; -} - async function settleInput(activeRunTask: Promise): Promise { await promiseWithTimeout(activeRunTask, { label: "Previous driver run input", diff --git a/src/core/driver-diagnostics.ts b/src/core/driver-diagnostics.ts index bbf1fd9..01178e6 100644 --- a/src/core/driver-diagnostics.ts +++ b/src/core/driver-diagnostics.ts @@ -39,7 +39,7 @@ export function createDriverDiagnosticEvent(input: DriverDiagnosticInput): Drive } export async function pushDriverDiagnosticEvent( - port: DriverRuntimeEventPort, + port: Pick, input: DriverDiagnosticInput, logger?: Logger, ): Promise { diff --git a/src/core/driver-permission-broker.ts b/src/core/driver-permission-broker.ts index 98ccaa2..8dd0a42 100644 --- a/src/core/driver-permission-broker.ts +++ b/src/core/driver-permission-broker.ts @@ -1,5 +1,6 @@ import { summarizeDriverPermissionRequest } from "../observability/driver-debug"; import type { Logger } from "../observability"; +import type { DriverPermissionRequest } from "../host-ports"; import type { DriverEventInput } from "../protocol/events"; import type { RunId } from "../protocol/id"; import { promiseWithTimeout, settlePromiseWithTimeout } from "../utils/async"; @@ -12,6 +13,7 @@ const PERMISSION_EVENT_DELIVERY_TIMEOUT_MS = 10_000; const PERMISSION_CANCEL_DELIVERY_TIMEOUT_MS = 1_500; const MAX_PENDING_PERMISSION_REQUEST_BYTES = 8 * 1_024 * 1_024; const MAX_PENDING_PERMISSION_REQUESTS = 1_024; +const MAX_PERMISSION_REQUEST_EVENT_BYTES = 512 * 1_024; const UTF8 = new TextEncoder(); export type PermissionDecision = "allow_once" | "reject_once"; @@ -39,14 +41,6 @@ interface PermissionCancellationDelivery { useTurnBudget(): void; } -export interface DriverPermissionRequest { - rawInput: string | null; - requestId: string; - title: string; - toolCallId: string | null; - toolKind: string | null; -} - export interface DriverPermissionBrokerOptions { eventDeliveryTimeoutMs?: number; interactiveRequests?: boolean; @@ -199,12 +193,17 @@ export class DriverPermissionBroker { throw new RangeError("Driver permission broker pending request byte budget is exhausted."); } - const runId = socket.currentRunId?.() ?? null; + const runId = socket.currentRunId(); const events: DriverEventInput[] = [ { kind: "permission.requested", payload: { + ...(input.agentId === undefined ? {} : { agentId: input.agentId }), + ...(input.blockedPath === undefined ? {} : { blockedPath: input.blockedPath }), + ...(input.decisionReason === undefined ? {} : { decisionReason: input.decisionReason }), details: input.rawInput, + ...(input.description === undefined ? {} : { description: input.description }), + ...(input.matchedAskRule === undefined ? {} : { matchedAskRule: input.matchedAskRule }), options: [], requestId: input.requestId, targetItemId: input.toolCallId, @@ -217,6 +216,13 @@ export class DriverPermissionBroker { ...(runId === null ? {} : { runId }), }, ]; + + if (UTF8.encode(JSON.stringify(events[0])).byteLength > MAX_PERMISSION_REQUEST_EVENT_BYTES) { + throw new RangeError( + `Driver permission request event exceeds ${String(MAX_PERMISSION_REQUEST_EVENT_BYTES)} UTF-8 bytes.`, + ); + } + const deferred = Promise.withResolvers(); const cancellationDelivery = new AbortController(); let cancellationDeliveryTimeout: ReturnType | null = null; diff --git a/src/core/driver-permission-policy.ts b/src/core/driver-permission-policy.ts index 8fc98b8..2a593ac 100644 --- a/src/core/driver-permission-policy.ts +++ b/src/core/driver-permission-policy.ts @@ -1,6 +1,7 @@ import type { DriverPermissionPolicy } from "../protocol/boot"; import type { DriverStartInput } from "../protocol/start"; -import type { DriverPermissionRequest, PermissionDecision } from "./driver-permission-broker"; +import type { DriverPermissionRequest } from "../host-ports"; +import type { PermissionDecision } from "./driver-permission-broker"; export type { DriverPermissionPolicy }; @@ -11,11 +12,10 @@ export function isDriverFullAccess(payload: DriverStartInput): boolean { /** * Build the permission handler the runtime context uses. * - * Under the `full_access` policy (the default) every tool call is approved - * synchronously inside the runtime — no control-plane round-trip, no - * `needs_approval` state churn, and no 5-minute reject-on-timeout window. The - * sandbox is the isolation boundary, so this is safe and removes permission - * latency from the critical path entirely. + * Under the `full_access` policy (the default), ordinary tool calls are + * approved synchronously inside the runtime. A provider-reported matched ask + * rule still delegates to the interactive handler so user policy cannot be + * bypassed. * * Under `supervised` the caller's interactive handler (the permission broker) * is used unchanged. @@ -28,7 +28,8 @@ export function createDriverPermissionRequestHandler(input: { ) => Promise; }): (request: DriverPermissionRequest, signal?: AbortSignal) => Promise { if (isDriverFullAccess(input.payload)) { - return async () => "allow_once"; + return async (request, signal) => + request.matchedAskRule === undefined ? "allow_once" : input.supervised(request, signal); } return input.supervised; diff --git a/src/core/driver-runtime-io.ts b/src/core/driver-runtime-io.ts index 19c14be..8df9a42 100644 --- a/src/core/driver-runtime-io.ts +++ b/src/core/driver-runtime-io.ts @@ -17,7 +17,7 @@ import type { } from "../runtime-command"; export interface DriverRuntimeEventPort { - currentRunId?(): RunId | null; + currentRunId(): RunId | null; pushEvents(input: { events: DriverEventInput[]; signal?: AbortSignal; @@ -83,10 +83,10 @@ export function assertDriverEventReceiptPrefix( /** * Owns one invocation's input and drains partial receipt prefixes. * - * Transport failures are returned to the caller and are not retried automatically. + * One transport failure after a valid receipt prefix is retried with the same event IDs. */ export async function pushLosslessEvents( - port: DriverRuntimeEventPort, + port: Pick, events: readonly DriverEventInput[], onAccepted?: (receipts: readonly DriverEventReceipt[]) => void, signal?: AbortSignal, @@ -94,12 +94,26 @@ export async function pushLosslessEvents( const receipts: DriverEventReceipt[] = []; const deadline = signal ?? AbortSignal.timeout(DRIVER_EVENT_DELIVERY_TIMEOUT_MS); let remaining = structuredClone(withSourceEventIds(events)); + let retryTransport = false; while (remaining.length > 0) { - const result = await port.pushEvents({ - events: remaining, - signal: deadline, - }); + deadline.throwIfAborted(); + let result: DriverEventBatchOutput; + + try { + result = await port.pushEvents({ + events: remaining, + signal: deadline, + }); + } catch (error) { + if (error instanceof DriverEventRejectedError || !retryTransport) { + throw error; + } + + retryTransport = false; + continue; + } + assertDriverEventReceiptPrefix(remaining, result.accepted); if (result.accepted.length === 0) { @@ -109,6 +123,7 @@ export async function pushLosslessEvents( onAccepted?.(result.accepted); receipts.push(...result.accepted); remaining = remaining.slice(result.accepted.length); + retryTransport = true; } return receipts; diff --git a/src/host-ports/index.ts b/src/host-ports/index.ts index 9a51fca..8fe2cef 100644 --- a/src/host-ports/index.ts +++ b/src/host-ports/index.ts @@ -9,6 +9,7 @@ import type { McpExternalToolEffectClaim, McpExternalToolEffectExecution, McpExternalToolExecutionResult, + RunError, RuntimeCommand, RuntimeCommandResult, } from "../runtime-command"; @@ -34,6 +35,7 @@ export interface AgentDriverEventSink { commandUpdate( input: { commandId: string; + error?: RunError; result?: RuntimeCommandResult; status: "accepted" | "cancelled" | "completed" | "failed"; }, @@ -47,7 +49,7 @@ export interface AgentDriverEventSink { }, signal: AbortSignal, ): Promise; - currentRunId?(): RunId | null; + currentRunId(): RunId | null; markExternalToolEffectUnknown?(input: { commandId: string }, signal: AbortSignal): Promise; pushEvents(input: { events: DriverEventInput[]; @@ -57,17 +59,28 @@ export interface AgentDriverEventSink { export interface AgentDriverPermissionPort { request( - input: { - rawInput: string | null; - requestId: string; - title: string; - toolCallId: string | null; - toolKind: string | null; - }, + input: DriverPermissionRequest, signal?: AbortSignal, ): Promise<"allow_once" | "reject_once">; } +export interface DriverPermissionRequest { + agentId?: string; + blockedPath?: string; + decisionReason?: string; + description?: string; + matchedAskRule?: { + readonly ruleContent?: string; + readonly source: string; + readonly toolName: string; + }; + rawInput: string | null; + requestId: string; + title: string; + toolCallId: string | null; + toolKind: string | null; +} + export interface AgentDriverMcpPort { execute( command: McpExecuteCommand, diff --git a/src/index.ts b/src/index.ts index 4d9e5ab..1f43909 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,6 +84,7 @@ export type { AgentDriverCommandSource, AgentDriverEventSink, AgentDriverPermissionPort, + DriverPermissionRequest, AgentDriverMaterializedSkill, AgentDriverMcpPort, AgentDriverSkillPort, diff --git a/src/infrastructure/runtime/driver-event-envelope.ts b/src/infrastructure/runtime/driver-event-envelope.ts index af3cc7c..8a6c653 100644 --- a/src/infrastructure/runtime/driver-event-envelope.ts +++ b/src/infrastructure/runtime/driver-event-envelope.ts @@ -1,7 +1,7 @@ import { DriverEventRejectedError } from "../../core/driver-runtime-io"; import type { DriverBootPayload } from "../../protocol/boot"; import type { DriverEventEnvelope, DriverEventInput } from "../../protocol/events"; -import { createDriverId, parseDriverId } from "../../protocol/id"; +import { createDriverId, parseDriverId, parseRunId } from "../../protocol/id"; import type { DriverInstanceId, EventId, SessionId, RunId } from "../../protocol/id"; import { isRuntimeEventEnvelope, toRuntimeEventInput } from "../../runtime-events"; @@ -11,14 +11,14 @@ function readExplicitSourceEventId(event: DriverEventInput): string | undefined : undefined; } -function parseRunId(value: string): RunId { - return parseDriverId(value, "Run ID") as RunId; -} - function readEventRunId(event: DriverEventInput, activeRunId: RunId | null): RunId | undefined { const { runId: eventRunId } = event; - return eventRunId === undefined ? (activeRunId ?? undefined) : parseRunId(eventRunId); + return eventRunId === null + ? undefined + : eventRunId === undefined + ? (activeRunId ?? undefined) + : parseRunId(eventRunId); } export function toDriverEventEnvelopes( @@ -54,13 +54,11 @@ export function toDriverEventEnvelopes( sourceEventId, }, event, - ).map( - (canonicalEvent): DriverEventEnvelope => ({ - event: canonicalEvent, - eventId: canonicalEvent.sourceEventId ?? canonicalEvent.id, - occurredAt: canonicalEvent.occurredAt, - }), - ); + ).map((canonicalEvent): DriverEventEnvelope => ({ + event: canonicalEvent, + eventId: canonicalEvent.sourceEventId ?? canonicalEvent.id, + occurredAt: canonicalEvent.occurredAt, + })); } catch (error) { throw new DriverEventRejectedError(sourceEventId, error); } diff --git a/src/observability/driver-debug-runtime.ts b/src/observability/driver-debug-runtime.ts index b129782..5194b7f 100644 --- a/src/observability/driver-debug-runtime.ts +++ b/src/observability/driver-debug-runtime.ts @@ -1,3 +1,4 @@ +import type { DriverPermissionRequest } from "../host-ports"; import type { RuntimeCommand, RuntimeCommandInput, RuntimeCommandResult } from "../runtime-command"; import { summarizeTextDigest } from "./driver-debug-paths"; @@ -80,14 +81,22 @@ export function summarizeRuntimeCommandResult( }; } -export function summarizeDriverPermissionRequest(input: { - rawInput: string | null; - requestId: string; - title: string; - toolCallId: string | null; - toolKind: string | null; -}): Record { +export function summarizeDriverPermissionRequest( + input: DriverPermissionRequest, +): Record { return { + agentId: input.agentId, + blockedPath: summarizeTextDigest(input.blockedPath ?? null), + decisionReason: summarizeTextDigest(input.decisionReason ?? null), + description: summarizeTextDigest(input.description ?? null), + matchedAskRule: + input.matchedAskRule === undefined + ? undefined + : { + ruleContent: summarizeTextDigest(input.matchedAskRule.ruleContent ?? null), + source: input.matchedAskRule.source, + toolName: input.matchedAskRule.toolName, + }, rawInput: summarizeTextDigest(input.rawInput), requestId: input.requestId, title: summarizeTextDigest(input.title), diff --git a/src/projections/cma/outbound.ts b/src/projections/cma/outbound.ts index 6e4d35d..3c96a15 100644 --- a/src/projections/cma/outbound.ts +++ b/src/projections/cma/outbound.ts @@ -61,10 +61,13 @@ export function projectDriverEventToCma(event: DriverEventInput): CmaOutboundEve switch (event.kind) { case "message.added": + case "message.cancelled": case "message.completed": case "message.delta": + case "message.failed": case "message.started": return [{ message: payload, sourceEventKind: event.kind, type: "agent.message" }]; + case "thought.cancelled": case "thought.completed": case "thought.delta": case "thought.started": @@ -75,7 +78,20 @@ export function projectDriverEventToCma(event: DriverEventInput): CmaOutboundEve return [ { requiresAction: { + ...(payload["agentId"] === undefined ? {} : { agentId: payload["agentId"] }), + ...(payload["blockedPath"] === undefined + ? {} + : { blockedPath: payload["blockedPath"] }), + ...(payload["decisionReason"] === undefined + ? {} + : { decisionReason: payload["decisionReason"] }), details: payload["details"], + ...(payload["description"] === undefined + ? {} + : { description: payload["description"] }), + ...(payload["matchedAskRule"] === undefined + ? {} + : { matchedAskRule: payload["matchedAskRule"] }), requestId: payload["requestId"], targetItemId: payload["targetItemId"], title: payload["title"], diff --git a/src/protocol/boot/testing.ts b/src/protocol/boot/testing.ts deleted file mode 100644 index cd8c7e2..0000000 --- a/src/protocol/boot/testing.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { DriverId, DriverInstanceId, EventId, MessageId, RunId, SessionId } from "../id"; -import { normalizeDriverId } from "../id"; -import type { - AccountId, - AgentDeploymentVersionId, - AgentId, - EnvironmentId, - EnvironmentRevisionId, - SandboxId, - SkillId, -} from "./host-ids"; - -function fixture(value: string): DriverId { - return normalizeDriverId(value, "Driver ID fixture"); -} - -export const DRIVER_ID_FIXTURES = { - account: fixture("01J00000000000000000000001") as AccountId, - agent: fixture("01J00000000000000000000002") as AgentId, - agentDeploymentVersion: fixture("01J00000000000000000000006") as AgentDeploymentVersionId, - driverInstance: fixture("01J00000000000000000000008") as DriverInstanceId, - environment: fixture("01J0000000000000000000000A") as EnvironmentId, - environmentRevision: fixture("01J0000000000000000000000B") as EnvironmentRevisionId, - event: fixture("01J0000000000000000000000G") as EventId, - message: fixture("01J0000000000000000000000M") as MessageId, - run: fixture("01J0000000000000000000000N") as RunId, - sandbox: fixture("01J0000000000000000000000J") as SandboxId, - session: fixture("01J0000000000000000000000K") as SessionId, - skill: fixture("01J0000000000000000000000P") as SkillId, -} as const; diff --git a/src/protocol/events/index.ts b/src/protocol/events/index.ts index e96405f..3c1ee95 100644 --- a/src/protocol/events/index.ts +++ b/src/protocol/events/index.ts @@ -1,9 +1,13 @@ import { timestampSchema } from "../../contract/common"; import { parseRuntimeEventEnvelope } from "./runtime-events"; import type { RuntimeEventEnvelope, RuntimeEventInputDraft } from "./runtime-events"; +import type { RunId } from "../id"; export type DriverEvent = RuntimeEventEnvelope; -export type DriverEventInput = RuntimeEventEnvelope | RuntimeEventInputDraft; +type DriverEventInputDraft = Omit & { + readonly runId?: RunId | null | undefined; +}; +export type DriverEventInput = RuntimeEventEnvelope | DriverEventInputDraft; export interface DriverEventEnvelope { readonly event: DriverEvent; diff --git a/src/protocol/events/runtime-event-types.ts b/src/protocol/events/runtime-event-types.ts index b2813d7..cb18098 100644 --- a/src/protocol/events/runtime-event-types.ts +++ b/src/protocol/events/runtime-event-types.ts @@ -23,7 +23,6 @@ export const RUNTIME_EVENT_KINDS = [ "file.indexed", "hook.completed", "hook.started", - "image.updated", "item.completed", "item.started", "item.updated", @@ -31,8 +30,10 @@ export const RUNTIME_EVENT_KINDS = [ "mcp.server.updated", "mcp.tool.updated", "message.added", + "message.cancelled", "message.completed", "message.delta", + "message.failed", "message.started", "model.routing.updated", "model.verification.updated", @@ -94,6 +95,7 @@ export const RUNTIME_EVENT_KINDS = [ "terminal.output.delta", "terminal.released", "thought.completed", + "thought.cancelled", "thought.delta", "thought.started", "tool.call.updated", diff --git a/src/protocol/events/runtime-event-validation.ts b/src/protocol/events/runtime-event-validation.ts index f0885c9..e35b002 100644 --- a/src/protocol/events/runtime-event-validation.ts +++ b/src/protocol/events/runtime-event-validation.ts @@ -1,4 +1,4 @@ -import { timestampSchema } from "../../contract/common"; +import { jsonValueSchema, timestampSchema } from "../../contract/common"; import type { DriverId } from "../id"; import { parseDriverId } from "../id"; import type { @@ -172,6 +172,34 @@ export function requireOptionalString( requireString(value, field, label); } +export function requireOptionalBoolean( + value: RuntimeEventRecord, + field: string, + label: RuntimeEventKind | string, +): void { + if (!(field in value) || value[field] === undefined) { + return; + } + + if (typeof value[field] !== "boolean") { + throw new Error(`${label} ${field} must be a boolean.`); + } +} + +export function requireOptionalJsonValue( + value: RuntimeEventRecord, + field: string, + label: RuntimeEventKind | string, +): void { + if (!(field in value) || value[field] === undefined) { + return; + } + + if (!jsonValueSchema.safeParse(value[field]).success) { + throw new Error(`${label} ${field} must be JSON-serializable.`); + } +} + export function requireOptionalContentString( value: RuntimeEventRecord, field: string, diff --git a/src/protocol/events/runtime-events.ts b/src/protocol/events/runtime-events.ts index c3a9877..204b0af 100644 --- a/src/protocol/events/runtime-events.ts +++ b/src/protocol/events/runtime-events.ts @@ -34,8 +34,10 @@ import { requireEnumValue, requireNonNegativeInt, requireNullableTimestamp, + requireOptionalBoolean, requireOptionalContentString, requireOptionalEnumValue, + requireOptionalJsonValue, requireOptionalNullableString, requireOptionalString, requireOptionalStringArray, @@ -102,7 +104,7 @@ const runStatuses = new Set([ "running", "waiting_input", ]); -const toolStatuses = new Set(["completed", "failed", "running"]); +const toolStatuses = new Set(["cancelled", "completed", "failed", "running"]); const fileChangeKinds = new Set(["delete", "upsert"]); export { isRuntimeEventRecord } from "./runtime-event-validation"; @@ -336,6 +338,16 @@ function admitRuntimeEventPayload( payload: unknown, ): unknown { switch (context.kind) { + case "agent.task.updated": { + const record = requirePayloadRecord(context.kind, payload); + requireString(record, "taskId", context.kind); + requireOptionalBoolean(record, "active", context.kind); + requireOptionalEnumValue(record, "status", toolStatuses, context.kind); + for (const field of ["activityKind", "agentId", "agentPath", "taskType", "title"]) { + requireOptionalString(record, field, context.kind); + } + return omitPayloadIdentity(record); + } case "diagnostic.reported": { const record = requirePayloadRecord(context.kind, payload); requireOptionalString(record, "code", context.kind); @@ -369,11 +381,26 @@ function admitRuntimeEventPayload( } requireOptionalString(record, "messageId", context.kind); + requireOptionalEnumValue( + record, + "level", + new Set(["info", "notice", "suggestion", "warning"]), + context.kind, + ); + requireOptionalBoolean(record, "preventContinuation", context.kind); requireOptionalEnumValue(record, "role", new Set(["agent", "user"]), context.kind); + requireOptionalString(record, "subtype", context.kind); + requireOptionalString(record, "toolCallId", context.kind); + + requireOptionalEnumValue(record, "phase", new Set(["commentary", "final"]), context.kind); + requireOptionalJsonValue(record, "memoryCitation", context.kind); + return omitPayloadIdentity(record); } + case "message.cancelled": case "message.completed": case "message.started": + case "thought.cancelled": case "thought.completed": case "thought.started": { const record = requirePayloadRecord(context.kind, payload); @@ -382,6 +409,14 @@ function admitRuntimeEventPayload( requireOptionalEnumValue(record, "role", new Set(["agent", "user"]), context.kind); return omitPayloadIdentity(record); } + case "message.failed": { + const record = requirePayloadRecord(context.kind, payload); + requireString(record, "messageId", context.kind); + requireOptionalEnumValue(record, "role", new Set(["agent", "user"]), context.kind); + const admitted = omitPayloadIdentity(record); + admitted["error"] = readRunError(context.kind, record["error"], "error"); + return admitted; + } case "thought.delta": { const record = requirePayloadRecord(context.kind, payload); @@ -404,9 +439,24 @@ function admitRuntimeEventPayload( const record = requirePayloadRecord(context.kind, payload); requireString(record, "requestId", context.kind); requireString(record, "title", context.kind); + requireOptionalString(record, "agentId", context.kind); + requireOptionalString(record, "blockedPath", context.kind); + requireOptionalString(record, "decisionReason", context.kind); + requireOptionalString(record, "description", context.kind); requireOptionalNullableString(record, "details", context.kind); requireOptionalNullableString(record, "targetItemId", context.kind); + if (record["matchedAskRule"] !== undefined) { + const matchedAskRule = requirePayloadRecord( + context.kind, + record["matchedAskRule"], + "matchedAskRule", + ); + requireString(matchedAskRule, "source", context.kind); + requireString(matchedAskRule, "toolName", context.kind); + requireOptionalString(matchedAskRule, "ruleContent", context.kind); + } + if ( "options" in record && record["options"] !== undefined && @@ -431,6 +481,14 @@ function admitRuntimeEventPayload( requireOptionalString(record, "optionKind", context.kind); return omitPayloadIdentity(record); } + case "session.info.updated": { + const record = requirePayloadRecord(context.kind, payload); + requireOptionalNullableString(record, "title", context.kind); + if (record["updatedAt"] !== undefined) { + requireNullableTimestamp(record, "updatedAt", context.kind, "updatedAt"); + } + return omitPayloadIdentity(record); + } case "run.cancel.requested": case "run.cancelled": case "run.completed": @@ -466,12 +524,19 @@ function admitRuntimeEventPayload( requireEnumValue(record, "status", toolStatuses, context.kind); requireString(record, "toolCallId", context.kind); requireOptionalContentString(record, "content", context.kind); + requireOptionalString(record, "agentId", context.kind); + requireOptionalString(record, "decisionReason", context.kind); + requireOptionalString(record, "decisionReasonType", context.kind); requireOptionalString(record, "kind", context.kind); requireOptionalString(record, "messageId", context.kind); + requireOptionalString(record, "name", context.kind); + requireOptionalString(record, "nonExecutionKind", context.kind); requireOptionalString(record, "parentMessageId", context.kind); requireOptionalString(record, "rawInput", context.kind); requireOptionalString(record, "rawOutput", context.kind); + requireOptionalJsonValue(record, "structuredOutput", context.kind); requireOptionalNullableString(record, "title", context.kind); + requireOptionalString(record, "userFeedback", context.kind); return omitPayloadIdentity(record); } default: { @@ -503,6 +568,7 @@ function readRunPayload( requireOptionalString(record, "targetRunId", context.kind); requireOptionalString(record, "userMessageId", context.kind); requireOptionalStringArray(record, "inputItemIds", context.kind); + requireOptionalJsonValue(record, "structuredOutput", context.kind); requireOptionalTimestamp(record, "completedAt", context.kind); requireOptionalTimestamp(record, "startedAt", context.kind); diff --git a/src/protocol/id/index.ts b/src/protocol/id/index.ts index c55bb40..3d36c37 100644 --- a/src/protocol/id/index.ts +++ b/src/protocol/id/index.ts @@ -58,6 +58,10 @@ export function parseDriverId(value: unknown, label?: string): DriverId { return normalizeDriverId(value, label); } +export function parseRunId(value: unknown): RunId { + return parseDriverId(value, "Run ID") as RunId; +} + function readRandomByte(): number { const crypto = (globalThis as typeof globalThis & { readonly crypto?: DriverIdCrypto }).crypto; diff --git a/src/protocol/orpc/index.ts b/src/protocol/orpc/index.ts index 3d0b2c3..424a347 100644 --- a/src/protocol/orpc/index.ts +++ b/src/protocol/orpc/index.ts @@ -1,13 +1,15 @@ +import { DRIVER_CAPABILITY_IDS, RUNTIME_COMMAND_STATUSES } from "../../runtime-command"; import type { DriverCapability, DriverCapabilityId, McpExecuteCommandResult, McpExternalToolEffectClaim, + RunError, RuntimeCommand, RuntimeCommandResult, RuntimeCommandStatus, } from "../../runtime-command"; -import type { DriverBootPayload } from "../boot"; +import { DRIVER_PROTOCOL_VERSION, type DriverBootPayload } from "../boot"; import { parseDriverEventEnvelope, type DriverEventEnvelope } from "../events"; import { isSupportedDriverRuntime } from "../runtime"; import type { DriverRuntime } from "../runtime"; @@ -90,12 +92,7 @@ export interface DriverLogBatchOutput { export interface DriverFailureInput { readonly driverInstanceId: string; - readonly error: { - readonly code: string; - readonly details: Record; - readonly message: string; - readonly retryable: boolean; - }; + readonly error: RunError; } export interface DriverCommandUpdateInput { @@ -287,8 +284,8 @@ function readProtocolVersion( ): DriverBootPayload["protocolVersion"] { const value = record["protocolVersion"]; - if (value !== 2) { - throw new TypeError("protocolVersion must be 2."); + if (value !== DRIVER_PROTOCOL_VERSION) { + throw new TypeError(`protocolVersion must be ${DRIVER_PROTOCOL_VERSION}.`); } return value; @@ -312,24 +309,8 @@ function readHeartbeatReason(value: unknown): DriverHeartbeatInput["reason"] { throw new TypeError("reason must be interval or ping."); } -const DRIVER_CAPABILITY_IDS = new Set([ - "custom_tool_execute", - "file_change", - "input_start", - "mcp_execute", - "native_resume", - "permission_request", - "session_stop", - "text_stream", - "thinking_stream", - "tool_stream", - "turn_cancel", - "usage", - "visible_activity", -]); - function readDriverCapabilityId(value: unknown): DriverCapabilityId { - if (typeof value === "string" && DRIVER_CAPABILITY_IDS.has(value as DriverCapabilityId)) { + if (typeof value === "string" && DRIVER_CAPABILITY_IDS.includes(value as DriverCapabilityId)) { return value as DriverCapabilityId; } @@ -413,18 +394,11 @@ export function parseDriverReadyInput(value: unknown): DriverReadyInput { }; } -const DRIVER_COMMAND_STATUSES = new Set([ - "accepted", - "cancelled", - "completed", - "delivered", - "expired", - "failed", - "queued", -]); - function readDriverCommandStatus(value: unknown): RuntimeCommandStatus { - if (typeof value === "string" && DRIVER_COMMAND_STATUSES.has(value as RuntimeCommandStatus)) { + if ( + typeof value === "string" && + RUNTIME_COMMAND_STATUSES.includes(value as RuntimeCommandStatus) + ) { return value as RuntimeCommandStatus; } diff --git a/src/runtime-command/index.ts b/src/runtime-command/index.ts index 55414d2..05e2d3c 100644 --- a/src/runtime-command/index.ts +++ b/src/runtime-command/index.ts @@ -8,14 +8,16 @@ export interface RunError { readonly retryable: boolean; } -export type RuntimeCommandStatus = - | "accepted" - | "cancelled" - | "completed" - | "delivered" - | "expired" - | "failed" - | "queued"; +export const RUNTIME_COMMAND_STATUSES = [ + "accepted", + "cancelled", + "completed", + "delivered", + "expired", + "failed", + "queued", +] as const; +export type RuntimeCommandStatus = (typeof RUNTIME_COMMAND_STATUSES)[number]; export interface RuntimeCommandInput { readonly text: string; @@ -114,20 +116,24 @@ export type McpExternalToolEffectClaim = export type RuntimeCommandResult = InputStartCommandResult | McpExecuteCommandResult | null; -export type DriverCapabilityId = - | "custom_tool_execute" - | "file_change" - | "input_start" - | "mcp_execute" - | "native_resume" - | "permission_request" - | "session_stop" - | "text_stream" - | "thinking_stream" - | "tool_stream" - | "turn_cancel" - | "usage" - | "visible_activity"; +export const DRIVER_CAPABILITY_IDS = [ + "custom_tool_execute", + "file_change", + "input_start", + "mcp_execute", + "native_resume", + "permission_request", + "session_stop", + "text_stream", + "thinking_stream", + "tool_stream", + "turn_cancel", + "usage", + "visible_activity", +] as const; +export type DriverCapabilityId = (typeof DRIVER_CAPABILITY_IDS)[number]; + +const MAX_RUNTIME_CONTROL_REASON_BYTES = 16 * 1_024; export interface DriverCapability { readonly details?: string | undefined; @@ -182,6 +188,13 @@ function readOptionalString(record: Record, field: string): str return value; } +function normalizeControlReason(reason: string): string { + const bytes = Buffer.byteLength(reason, "utf8"); + return bytes <= MAX_RUNTIME_CONTROL_REASON_BYTES + ? reason + : `Runtime command reason exceeded ${String(MAX_RUNTIME_CONTROL_REASON_BYTES)} UTF-8 bytes (received ${String(bytes)}).`; +} + function readRuntimeCommandInput(value: unknown): RuntimeCommandInput { const record = readRecord(value, "input"); const attachmentIds = record["attachmentIds"]; @@ -242,7 +255,7 @@ export function parseRuntimeCommand(value: unknown): RuntimeCommand { return { commandId: readNonEmptyString(record, "commandId"), kind, - reason: readNonEmptyString(record, "reason"), + reason: normalizeControlReason(readNonEmptyString(record, "reason")), }; case "turn.cancel": { const reason = readOptionalString(record, "reason"); @@ -250,7 +263,7 @@ export function parseRuntimeCommand(value: unknown): RuntimeCommand { return { commandId: readNonEmptyString(record, "commandId"), kind, - ...(reason === undefined ? {} : { reason }), + ...(reason === undefined ? {} : { reason: normalizeControlReason(reason) }), }; } default: diff --git a/src/runtimes/acp/acp-assistant-transcript-state.ts b/src/runtimes/acp/acp-assistant-transcript-state.ts index 3bf37f6..a4f9ddd 100644 --- a/src/runtimes/acp/acp-assistant-transcript-state.ts +++ b/src/runtimes/acp/acp-assistant-transcript-state.ts @@ -2,6 +2,7 @@ import type { StopReason } from "@agentclientprotocol/sdk"; import type { DriverEventInput } from "../../protocol/events"; import type { RunId } from "../../protocol/id"; +import { chunkJsonText } from "../provider-json"; import { RuntimeAssistantMessageIdIndex } from "../runtime-turn-transcript"; import { toPermissionRequest } from "./acp-permission-events"; import type { AcpPermissionTranslation } from "./acp-permission-events"; @@ -16,13 +17,21 @@ import { toUsageEvents, } from "./acp-session-events"; import { AcpToolEventState, toRuntimeToolStatus } from "./acp-tool-events"; -import { isRecord, readNonEmptyString, readRecord, readString } from "./acp-types"; +import { + assertBoundedLosslessEvents, + isRecord, + MAX_ACP_LOSSLESS_EVENT_BYTES, + readNonEmptyString, + readRecord, + readString, +} from "./acp-types"; import type { JsonObject } from "./acp-types"; -export interface AcpTurnEventStateInput { +const MAX_ACP_MESSAGE_EVENT_TEXT_BYTES = MAX_ACP_LOSSLESS_EVENT_BYTES - 1_024; + +export interface AcpAssistantTranscriptStateInput { readonly messageId: string; readonly runId: RunId; - readonly sessionId: string; } interface AcpAssistantMessageState { @@ -43,7 +52,6 @@ export class AcpAssistantTranscriptState { #promptMessageId: string | null = null; #runId: RunId | null = null; #sequence = 0; - #sessionId: string | null = null; readonly #settledAssistantNativeMessageIds = new Set(); #unidentifiedAssistantMessageSequence = 0; #thoughtCompleted = false; @@ -57,14 +65,13 @@ export class AcpAssistantTranscriptState { return this.#runId; } - begin(input: AcpTurnEventStateInput): void { + begin(input: AcpAssistantTranscriptStateInput): void { this.#activeAssistantMessage = null; this.#assistantMessageIds.reset(); this.#lastCompletedAssistantMessage = null; this.#promptMessageId = input.messageId; this.#runId = input.runId; this.#sequence = 0; - this.#sessionId = input.sessionId; this.#settledAssistantNativeMessageIds.clear(); this.#unidentifiedAssistantMessageSequence = 0; this.#thoughtCompleted = false; @@ -82,7 +89,6 @@ export class AcpAssistantTranscriptState { this.#promptMessageId = null; this.#runId = null; this.#sequence = 0; - this.#sessionId = null; this.#settledAssistantNativeMessageIds.clear(); this.#unidentifiedAssistantMessageSequence = 0; this.#thoughtCompleted = false; @@ -93,33 +99,79 @@ export class AcpAssistantTranscriptState { this.#tools.clear(); } + checkpoint(): () => void { + const activeAssistantMessage = + this.#activeAssistantMessage === null ? null : { ...this.#activeAssistantMessage }; + const lastCompletedAssistantMessage = + this.#lastCompletedAssistantMessage === null + ? null + : { ...this.#lastCompletedAssistantMessage }; + const sequence = this.#sequence; + const settledAssistantNativeMessageIds = new Set(this.#settledAssistantNativeMessageIds); + const thoughtCompleted = this.#thoughtCompleted; + const thoughtFallbackNativeMessageId = this.#thoughtFallbackNativeMessageId; + const thoughtFallbackText = this.#thoughtFallbackText; + const thoughtStarted = this.#thoughtStarted; + const unidentifiedAssistantMessageSequence = this.#unidentifiedAssistantMessageSequence; + const restoreTools = this.#tools.checkpoint(); + + return () => { + this.#activeAssistantMessage = activeAssistantMessage; + this.#lastCompletedAssistantMessage = lastCompletedAssistantMessage; + this.#sequence = sequence; + this.#settledAssistantNativeMessageIds.clear(); + settledAssistantNativeMessageIds.forEach((id) => + this.#settledAssistantNativeMessageIds.add(id), + ); + this.#thoughtCompleted = thoughtCompleted; + this.#thoughtFallbackNativeMessageId = thoughtFallbackNativeMessageId; + this.#thoughtFallbackText = thoughtFallbackText; + this.#thoughtStarted = thoughtStarted; + this.#unidentifiedAssistantMessageSequence = unidentifiedAssistantMessageSequence; + restoreTools(); + }; + } + completePrompt(stopReason: StopReason, usage: unknown): DriverEventInput[] { const events: DriverEventInput[] = []; const runId = this.#requireRunId(); + const terminalError = + stopReason === "end_turn" || stopReason === "cancelled" + ? null + : `ACP prompt stopped with ${stopReason}.`; events.push(...this.#promoteThought()); - events.push(...this.#finishMessage()); + events.push( + ...(stopReason === "end_turn" + ? this.#finishMessage() + : this.#finishMessageWithOutcome(stopReason, terminalError)), + ); if (this.#thoughtStarted && !this.#thoughtCompleted) { this.#thoughtCompleted = true; events.push({ - kind: "thought.completed", + kind: stopReason === "end_turn" ? "thought.completed" : "thought.cancelled", payload: { channel: "summary", + ...(stopReason === "end_turn" + ? {} + : { reason: terminalError ?? "cancelled", stopReason }), thoughtId: this.#requireThoughtId(), }, runId, }); } - const toolStatus = stopReason === "cancelled" ? "failed" : "completed"; - const toolError = - stopReason === "cancelled" ? "Turn cancelled before tool completion." : undefined; events.push( ...this.#tools.completeOpen({ + ...(terminalError === null ? {} : { error: terminalError }), runId, - status: toolStatus, - ...(toolError === undefined ? {} : { error: toolError }), + status: + stopReason === "end_turn" + ? "completed" + : stopReason === "cancelled" + ? "cancelled" + : "failed", }), ); @@ -148,6 +200,19 @@ export class AcpAssistantTranscriptState { }, runId, }); + } else if (terminalError !== null) { + events.push({ + kind: "run.failed", + payload: { + error: { + code: `acp.${stopReason}`, + message: terminalError, + }, + recoverable: false, + stopReason, + }, + runId, + }); } else if (emptyTurn) { events.push({ kind: "run.failed", @@ -169,7 +234,6 @@ export class AcpAssistantTranscriptState { ? {} : { finalMessageId: finalMessage.id, - finalMessageText: finalMessage.text, }), stopReason, }, @@ -177,8 +241,7 @@ export class AcpAssistantTranscriptState { }); } - this.clear(); - return events; + return assertBoundedLosslessEvents(events); } failPrompt(error: { code: string; message: string; recoverable?: boolean }): DriverEventInput[] { @@ -189,16 +252,23 @@ export class AcpAssistantTranscriptState { return []; } + const originalMessageUtf8Bytes = Buffer.byteLength(error.message, "utf8"); + const message = + originalMessageUtf8Bytes <= 16 * 1_024 + ? error.message + : `ACP failure exceeded durable event capacity (originalMessageUtf8Bytes=${originalMessageUtf8Bytes}).`; const events: DriverEventInput[] = []; const thoughtId = this.#thoughtId; - events.push(...this.#finishMessage()); + events.push(...this.#failMessage({ ...error, message })); if (this.#thoughtStarted && !this.#thoughtCompleted && thoughtId !== null) { + this.#thoughtCompleted = true; events.push({ - kind: "thought.completed", + kind: "thought.cancelled", payload: { channel: "summary", + reason: message, thoughtId, }, runId, @@ -207,7 +277,7 @@ export class AcpAssistantTranscriptState { events.push( ...this.#tools.completeOpen({ - error: error.message, + error: message, runId, status: "failed", }), @@ -218,15 +288,15 @@ export class AcpAssistantTranscriptState { payload: { error: { code: error.code, - message: error.message, + ...(message === error.message ? {} : { details: { originalMessageUtf8Bytes } }), + message, }, recoverable: error.recoverable ?? false, }, runId, }); - this.clear(); - return events; + return assertBoundedLosslessEvents(events); } translateUpdate(params: unknown): DriverEventInput[] { @@ -234,43 +304,56 @@ export class AcpAssistantTranscriptState { const update = readRecord(record, "update"); const sessionUpdate = readString(update, "sessionUpdate"); + let events: DriverEventInput[]; + switch (sessionUpdate) { case "agent_message_chunk": { - return this.#messageChunk(update); + events = this.#messageChunk(update); + break; } case "agent_thought_chunk": { - return this.#thoughtChunk(update); + events = this.#thoughtChunk(update); + break; } case "available_commands_update": { - return toCommandEvents(update); + events = toCommandEvents(update); + break; } case "config_option_update": { - return toConfigEvents(update); + events = toConfigEvents(update); + break; } case "current_mode_update": { - return toModeEvents(update); + events = toModeEvents(update); + break; } case "plan": { - return toPlanEvents(update); + events = toPlanEvents(update); + break; } case "session_info_update": { - return toInfoEvents(update); + events = toInfoEvents(update); + break; } case "tool_call": case "tool_call_update": { - return this.#tool(update, sessionUpdate); + events = this.#tool(update, sessionUpdate); + break; } case "usage_update": { - return toUsageEvents(update); + events = toUsageEvents(update); + break; } case "user_message_chunk": case undefined: case null: { - return []; + events = []; + break; } default: { - return [ + events = [ { + delivery: "best_effort", kind: "diagnostic.reported", payload: { message: `Unsupported ACP session update: ${sessionUpdate}.`, @@ -280,8 +363,11 @@ export class AcpAssistantTranscriptState { visibility: "owner_debug", }, ]; + break; } } + + return assertBoundedLosslessEvents(events); } translatePermission(input: { params: unknown; requestId: string }): AcpPermissionTranslation { @@ -293,7 +379,10 @@ export class AcpAssistantTranscriptState { }); if (runId === null || translation.toolCall === null) { - return translation; + return { + ...translation, + events: assertBoundedLosslessEvents(translation.events), + }; } this.#tools.patch({ @@ -304,22 +393,22 @@ export class AcpAssistantTranscriptState { return { ...translation, - events: [ + events: assertBoundedLosslessEvents([ ...this.#ensureToolParentMessage(translation.targetItemId), ...this.#tools.ensureStarted({ parentMessageId: this.#toolParentMessageId(), runId, - title: translation.title, + title: translation.request.title, toolCallId: translation.targetItemId, }), ...translation.events, - ], + ]), }; } #nextEventId(kind: string): string { this.#sequence += 1; - return `acp:${this.#sessionId ?? "session"}:${this.#runId ?? "run"}:${kind}:${this.#sequence}`; + return `acp:${this.#runId ?? "run"}:${kind}:${this.#sequence}`; } #messageChunk(update: JsonObject | null): DriverEventInput[] { @@ -494,24 +583,7 @@ export class AcpAssistantTranscriptState { } started.message.text += contentDelta; - return [ - ...started.events, - { - delivery: "best_effort", - kind: "message.delta", - payload: { - contentBlock: { - text: contentDelta, - type: "text", - }, - contentDelta, - messageId: started.message.id, - role: "agent", - }, - runId: this.#requireRunId(), - sourceEventId: this.#nextEventId("agent-thought-fallback-message"), - }, - ]; + return started.events; } #clearThoughtFallback(): void { @@ -541,7 +613,36 @@ export class AcpAssistantTranscriptState { this.#lastCompletedAssistantMessage = null; } + const chunks = + message.text.length === 0 + ? [] + : chunkJsonText(message.text, MAX_ACP_MESSAGE_EVENT_TEXT_BYTES); + return [ + ...(chunks.length === 0 + ? [] + : [ + { + delivery: "lossless" as const, + kind: "message.added" as const, + payload: { + content: chunks[0]!, + messageId: message.id, + role: "agent", + }, + runId: this.#requireRunId(), + }, + ]), + ...chunks.slice(1).map((contentDelta): DriverEventInput => ({ + delivery: "lossless", + kind: "message.delta", + payload: { + contentDelta, + messageId: message.id, + role: "agent", + }, + runId: this.#requireRunId(), + })), { kind: "message.completed", payload: { @@ -553,6 +654,83 @@ export class AcpAssistantTranscriptState { ]; } + #finishMessageWithOutcome(stopReason: StopReason, error: string | null): DriverEventInput[] { + const message = this.#activeAssistantMessage; + + if (message === null) { + return []; + } + + this.#activeAssistantMessage = null; + this.#lastCompletedAssistantMessage = null; + + if (message.nativeMessageId !== null) { + this.#settledAssistantNativeMessageIds.add(message.nativeMessageId); + } + + if (stopReason === "cancelled") { + return [ + { + kind: "message.cancelled", + payload: { messageId: message.id, reason: "cancelled", role: "agent", stopReason }, + runId: this.#requireRunId(), + }, + ]; + } + + return [ + { + kind: "message.failed", + payload: { + error: { + code: `acp.${stopReason}`, + message: error ?? `ACP prompt stopped with ${stopReason}.`, + retryable: false, + }, + messageId: message.id, + role: "agent", + stopReason, + }, + runId: this.#requireRunId(), + }, + ]; + } + + #failMessage(error: { + readonly code: string; + readonly message: string; + readonly recoverable?: boolean; + }): DriverEventInput[] { + const message = this.#activeAssistantMessage; + + if (message === null) { + return []; + } + + this.#activeAssistantMessage = null; + this.#lastCompletedAssistantMessage = null; + + if (message.nativeMessageId !== null) { + this.#settledAssistantNativeMessageIds.add(message.nativeMessageId); + } + + return [ + { + kind: "message.failed", + payload: { + error: { + code: error.code, + message: error.message, + retryable: error.recoverable ?? false, + }, + messageId: message.id, + role: "agent", + }, + runId: this.#requireRunId(), + }, + ]; + } + #startMessage(nativeMessageId: string): AcpAssistantMessageStart | null { if (this.#settledAssistantNativeMessageIds.has(nativeMessageId)) { return null; diff --git a/src/runtimes/acp/acp-client-request-handler.ts b/src/runtimes/acp/acp-client-request-handler.ts index 7c6656e..244b416 100644 --- a/src/runtimes/acp/acp-client-request-handler.ts +++ b/src/runtimes/acp/acp-client-request-handler.ts @@ -21,13 +21,14 @@ import type { import type { DriverEventInput } from "../../protocol/events"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; -import { shouldIgnoreReplay } from "./acp-event-translator"; -import type { AcpPermissionOption, AcpTurnEventState } from "./acp-event-translator"; +import type { AcpAssistantTranscriptState } from "./acp-assistant-transcript-state"; import { AcpFileSystem } from "./acp-file-system"; import { AcpPathScope } from "./acp-path-scope"; +import type { AcpPermissionOption, AcpPermissionTranslation } from "./acp-permission-events"; +import { shouldIgnoreReplay } from "./acp-session-events"; import { AcpSessionUpdateInbox, type AcpSessionUpdateScope } from "./acp-session-update-inbox"; import { AcpTerminalManager } from "./acp-terminal-manager"; -import { isRecord, raceWithAbort, readNonEmptyString, stringifyForDisplay } from "./acp-types"; +import { isRecord, raceWithAbort, readNonEmptyString } from "./acp-types"; interface AcpClientRequestHandlerOptions { readonly allowedRoots: readonly string[]; @@ -37,7 +38,7 @@ interface AcpClientRequestHandlerOptions { nativeSessionId(): string | null; onUpdateFailure(error: Error): void; push(context: AgentDriverContext, reason: string, events: DriverEventInput[]): Promise; - readonly turnEvents: AcpTurnEventState; + readonly turnEvents: AcpAssistantTranscriptState; } export class AcpClientRequestHandler { @@ -48,10 +49,11 @@ export class AcpClientRequestHandler { readonly #push: AcpClientRequestHandlerOptions["push"]; #permissionIngressClosed = false; #stopping = false; + #transcriptTail: Promise = Promise.resolve(); #turnUpdateIngressClosed = false; readonly #updateInbox: AcpSessionUpdateInbox; readonly #terminalManager: AcpTerminalManager; - readonly #turnEvents: AcpTurnEventState; + readonly #turnEvents: AcpAssistantTranscriptState; constructor(options: AcpClientRequestHandlerOptions) { this.#isCancelling = options.isCancelling; @@ -174,6 +176,10 @@ export class AcpClientRequestHandler { return this.#terminalManager.create(context, params, signal); } + beginTurnTerminals(): number { + return this.#terminalManager.beginTurn(); + } + async killTerminal( context: AgentDriverContext, params: KillTerminalRequest, @@ -210,6 +216,10 @@ export class AcpClientRequestHandler { await this.#terminalManager.stopAll(context); } + async stopTurnTerminals(context: AgentDriverContext, turn: number): Promise { + await this.#terminalManager.stopTurn(context, turn); + } + async closeUpdates(): Promise { this.#stopping = true; await this.#updateInbox.close(); @@ -279,25 +289,22 @@ export class AcpClientRequestHandler { return { outcome: { outcome: "cancelled" } }; } - const translation = this.#turnEvents.translatePermission({ - params, - requestId, - }); - const toolEvents = translation.events.filter((event) => event.kind !== "permission.requested"); - - if (toolEvents.length > 0) { - await raceWithAbort( - track(this.#push(context, "driver.acp.permission.tool", toolEvents)), - signal, - ); - } + const translation = await raceWithAbort( + this.#withTranscriptTransaction(async () => { + signal?.throwIfAborted(); + const translated = this.#turnEvents.translatePermission({ params, requestId }); + if (translated.events.length > 0) { + await track(this.#push(context, "driver.acp.permission.tool", translated.events)); + } + return translated; + }), + signal, + ); let chosen: AcpPermissionOption | null = null; if (!this.#isCancelling() && !signal?.aborted) { - const permission = track( - this.#resolvePermission(context, requestId, translation.options, params, signal), - ); + const permission = track(this.#resolvePermission(context, translation, signal)); chosen = await raceWithAbort(permission, signal); } const resolvedOption = this.#isCancelling() || signal?.aborted ? null : chosen; @@ -332,13 +339,33 @@ export class AcpClientRequestHandler { return; } - const events = this.#turnEvents.translateUpdate(params); + await this.#withTranscriptTransaction(async () => { + const events = this.#turnEvents.translateUpdate(params); - if (events.length === 0) { - return; - } + if (events.length === 0) { + return; + } - await this.#push(context, "driver.acp.session.update", events); + await this.#push(context, "driver.acp.session.update", events); + }); + } + + #withTranscriptTransaction(operation: () => Promise): Promise { + const transaction = this.#transcriptTail.then(async () => { + const restore = this.#turnEvents.checkpoint(); + + try { + return await operation(); + } catch (error) { + restore(); + throw error; + } + }); + this.#transcriptTail = transaction.then( + () => {}, + () => {}, + ); + return transaction; } #assertSession(method: string, params: unknown): void { @@ -357,33 +384,17 @@ export class AcpClientRequestHandler { async #resolvePermission( context: AgentDriverContext, - requestId: string, - options: readonly AcpPermissionOption[], - params: unknown, + translation: AcpPermissionTranslation, signal?: AbortSignal, ): Promise { - const allow = options.find((option) => option.kind === "allow_once") ?? null; - const reject = options.find((option) => option.kind === "reject_once") ?? null; + const allow = translation.options.find((option) => option.kind === "allow_once") ?? null; + const reject = translation.options.find((option) => option.kind === "reject_once") ?? null; if (allow === null && reject === null) { return null; } - const record = isRecord(params) ? params : {}; - const toolCall = isRecord(record["toolCall"]) ? record["toolCall"] : {}; - const decision = await context.ports.permission.request( - { - rawInput: stringifyForDisplay(toolCall["rawInput"]), - requestId, - title: - readNonEmptyString(toolCall, "title") ?? - readNonEmptyString(toolCall, "kind") ?? - "Allow tool call?", - toolCallId: readNonEmptyString(toolCall, "toolCallId"), - toolKind: readNonEmptyString(toolCall, "kind"), - }, - signal, - ); + const decision = await context.ports.permission.request(translation.request, signal); return decision === "allow_once" ? allow : reject; } diff --git a/src/runtimes/acp/acp-configuration.ts b/src/runtimes/acp/acp-configuration.ts index b09ac60..029e6c6 100644 --- a/src/runtimes/acp/acp-configuration.ts +++ b/src/runtimes/acp/acp-configuration.ts @@ -1,5 +1,10 @@ import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk"; -import type { AgentCapabilities, ClientCapabilities, McpServer } from "@agentclientprotocol/sdk"; +import type { + AgentCapabilities, + AuthMethod, + ClientCapabilities, + McpServer, +} from "@agentclientprotocol/sdk"; import { basename } from "node:path"; import type { DriverExecutionSessionContext } from "../../protocol/boot"; @@ -207,7 +212,7 @@ export function readResumeId(payload: DriverStartInput): string | null { } export function resolveAuthMethod( - authMethods: readonly { readonly id: string }[], + authMethods: readonly AuthMethod[], env: Record, ): string | null { const requestedMethodId = env["MOSOO_ACP_AUTH_METHOD_ID"]?.trim(); @@ -216,11 +221,17 @@ export function resolveAuthMethod( return null; } - if (authMethods.some((method) => method.id === requestedMethodId)) { - return requestedMethodId; + const method = authMethods.find((candidate) => candidate.id === requestedMethodId); + + if (method === undefined) { + throw new Error(`Configured ACP auth method is not advertised: ${requestedMethodId}.`); + } + + if ("type" in method && method.type === "terminal") { + throw new Error(`Configured ACP auth method requires unsupported terminal auth: ${method.id}.`); } - throw new Error(`Configured ACP auth method is not advertised: ${requestedMethodId}.`); + return method.id; } export function toRequestMeta(input: { diff --git a/src/runtimes/acp/acp-driver-backend.ts b/src/runtimes/acp/acp-driver-backend.ts index c00dc95..89da6fb 100644 --- a/src/runtimes/acp/acp-driver-backend.ts +++ b/src/runtimes/acp/acp-driver-backend.ts @@ -49,7 +49,7 @@ import { supportsSessionLoad, supportsSessionResume, } from "./acp-configuration"; -import { toAuthEvent, toInitializeEvents, toSessionReadyEvents } from "./acp-event-translator"; +import { toAuthEvent, toInitializeEvents, toSessionReadyEvents } from "./acp-session-events"; import { setupAcpSession } from "./acp-session-setup"; import { withAcpStartupStage } from "./acp-startup"; import { AcpTurnController } from "./acp-turn-controller"; @@ -94,6 +94,8 @@ export class AcpDriverBackend implements AgentDriverBackend { this.#turnController = new AcpTurnController( (context, reason, events) => this.#push(context, reason, events), (context) => this.#recycleCancelledTurn(context), + (context, reason, closures, terminal) => + this.#eventPublisher.pushTerminal(context, reason, closures, terminal), ); this.#clientRequests = new AcpClientRequestHandler({ allowedRoots: payload.execution.session.additionalDirectories, @@ -356,13 +358,6 @@ export class AcpDriverBackend implements AgentDriverBackend { signal, ); - if (setup.droppedAdditionalDirectories.length > 0) { - context.logger.warn("driver.acp.session.additional_directories_dropped", { - count: setup.droppedAdditionalDirectories.length, - reason: "agent_capability_missing", - }); - } - if ( requiredResumeSessionId !== null && (setup.mode !== "resumed" || setup.sessionId !== requiredResumeSessionId) diff --git a/src/runtimes/acp/acp-event-translator.ts b/src/runtimes/acp/acp-event-translator.ts deleted file mode 100644 index 6c9ce52..0000000 --- a/src/runtimes/acp/acp-event-translator.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AcpAssistantTranscriptState } from "./acp-assistant-transcript-state"; - -export type { AcpTurnEventStateInput } from "./acp-assistant-transcript-state"; -export { AcpAssistantTranscriptState } from "./acp-assistant-transcript-state"; -export type { AcpPermissionOption, AcpPermissionTranslation } from "./acp-permission-events"; -export { toPermissionRequest, toPermissionResolvedEvent } from "./acp-permission-events"; -export { - shouldIgnoreReplay, - toAuthEvent, - toInitializeEvents, - toPromptStartEvents, - toSessionReadyEvents, -} from "./acp-session-events"; - -/** Compatibility façade for the existing runtime boundary. */ -export class AcpTurnEventState extends AcpAssistantTranscriptState {} diff --git a/src/runtimes/acp/acp-input-limit.ts b/src/runtimes/acp/acp-input-limit.ts index fd64c65..51ec358 100644 --- a/src/runtimes/acp/acp-input-limit.ts +++ b/src/runtimes/acp/acp-input-limit.ts @@ -8,23 +8,47 @@ export function limitAcpInput( throw new RangeError("ACP message byte limit must be a positive safe integer."); } + const decoder = new TextDecoder("utf-8", { fatal: true }); let pendingBytes = 0; + + const append = (bytes: Uint8Array, lineEnded: boolean): void => { + if (bytes.byteLength === 0 && !lineEnded) { + return; + } + + pendingBytes += bytes.byteLength; + if (pendingBytes > maxMessageBytes) { + throw new Error(`ACP message exceeds ${maxMessageBytes} bytes.`); + } + + decoder.decode(bytes, { stream: !lineEnded }); + if (lineEnded) { + pendingBytes = 0; + } + }; + return input.pipeThrough( new TransformStream({ transform(chunk, controller) { - for (const byte of chunk) { - if (byte === 0x0a) { - pendingBytes = 0; - } else { - pendingBytes += 1; - if (pendingBytes > maxMessageBytes) { - throw new Error(`ACP message exceeds ${maxMessageBytes} bytes.`); - } - } + let lineStart = 0; + for ( + let newline = chunk.indexOf(0x0a); + newline >= 0; + newline = chunk.indexOf(0x0a, lineStart) + ) { + append(chunk.subarray(lineStart, newline), true); + lineStart = newline + 1; } + append(chunk.subarray(lineStart), false); + controller.enqueue(chunk); }, + flush() { + if (pendingBytes > 0) { + decoder.decode(); + } + }, }), ); } diff --git a/src/runtimes/acp/acp-lifecycle-events.ts b/src/runtimes/acp/acp-lifecycle-events.ts index 2252a4c..fa6aa9a 100644 --- a/src/runtimes/acp/acp-lifecycle-events.ts +++ b/src/runtimes/acp/acp-lifecycle-events.ts @@ -2,22 +2,21 @@ import type { InitializeResponse } from "@agentclientprotocol/sdk"; import type { DriverEventInput } from "../../protocol/events"; import type { RunId } from "../../protocol/id"; -import { isRecord, readNumber, readRecord, readString } from "./acp-types"; -import type { JsonObject } from "./acp-types"; import { - toCapabilityEvents, - toConfigEvents, - toModeEvents, - toModelEvents, -} from "./acp-session-update-events"; - -// OpenCode's ACP usage reports fresh input tokens with cache read/write as -// separate buckets (Anthropic-style), not an input total that includes them. -const ACP_USAGE_CONTRACT = "anthropic_bucketed"; + ACP_USAGE_CONTRACT, + assertBoundedLosslessEvents, + isRecord, + readNumber, + readRecord, + readString, +} from "./acp-types"; +import type { JsonObject } from "./acp-types"; +import { toConfigEvents, toModeEvents } from "./acp-session-update-events"; export function toInitializeEvents(result: InitializeResponse): DriverEventInput[] { const events: DriverEventInput[] = [ { + delivery: "best_effort", kind: "runtime.capabilities.updated", payload: { capabilities: result.agentCapabilities ?? {}, @@ -29,6 +28,7 @@ export function toInitializeEvents(result: InitializeResponse): DriverEventInput if (result.authMethods !== undefined && result.authMethods.length > 0) { events.push({ + delivery: "best_effort", kind: "auth.methods.updated", payload: { methods: result.authMethods, @@ -45,7 +45,7 @@ export function toPromptStartEvents(input: { runId: RunId; text: string; }): DriverEventInput[] { - return [ + return assertBoundedLosslessEvents([ { actor: "user", kind: "message.added", @@ -80,7 +80,7 @@ export function toPromptStartEvents(input: { }, runId: input.runId, }, - ]; + ]); } export function toSessionReadyEvents(input: { @@ -88,7 +88,7 @@ export function toSessionReadyEvents(input: { nativeSessionId: string; setup: JsonObject; }): DriverEventInput[] { - return [ + return assertBoundedLosslessEvents([ { kind: input.mode === "created" ? "session.created" : "session.resumed", payload: @@ -109,25 +109,25 @@ export function toSessionReadyEvents(input: { }, visibility: "owner_debug", }, - ...toModeEvents(input.setup), - ...toModelEvents(input.setup), + ...toModeEvents(readRecord(input.setup, "modes")), ...toConfigEvents(input.setup), - ...toCapabilityEvents(input.setup), - ]; + ]); } export function toAuthEvent(input: { methodId: string; status: "authenticated" | "failed"; }): DriverEventInput { - return { - kind: "auth.session.updated", - payload: { - methodId: input.methodId, - status: input.status, + return assertBoundedLosslessEvents([ + { + kind: "auth.session.updated", + payload: { + methodId: input.methodId, + status: input.status, + }, + visibility: "owner_debug", }, - visibility: "owner_debug", - }; + ])[0]!; } export function shouldIgnoreReplay(params: unknown): boolean { diff --git a/src/runtimes/acp/acp-permission-events.ts b/src/runtimes/acp/acp-permission-events.ts index f2ded69..98188e1 100644 --- a/src/runtimes/acp/acp-permission-events.ts +++ b/src/runtimes/acp/acp-permission-events.ts @@ -1,5 +1,6 @@ import type { DriverEventInput } from "../../protocol/events"; import type { RunId } from "../../protocol/id"; +import type { DriverPermissionRequest } from "../../host-ports"; import { toRuntimeToolStatus, toToolCallPayload } from "./acp-tool-events"; import { isRecord, @@ -11,12 +12,10 @@ import { import type { JsonObject } from "./acp-types"; export interface AcpPermissionTranslation { - readonly defaultOptionId: string | null; readonly events: DriverEventInput[]; readonly options: readonly AcpPermissionOption[]; - readonly requestId: string; + readonly request: DriverPermissionRequest; readonly targetItemId: string; - readonly title: string; readonly toolCall: JsonObject | null; } @@ -34,7 +33,8 @@ export function toPermissionRequest(input: { const params = isRecord(input.params) ? input.params : {}; const toolCall = readRecord(params, "toolCall"); const options = normalizePermissionOptions(params["options"]); - const toolCallId = readNonEmptyString(toolCall, "toolCallId") ?? input.requestId; + const nativeToolCallId = readNonEmptyString(toolCall, "toolCallId"); + const toolCallId = nativeToolCallId ?? input.requestId; const title = readNonEmptyString(toolCall, "title") ?? readNonEmptyString(toolCall, "kind") ?? @@ -53,54 +53,21 @@ export function toPermissionRequest(input: { }); } - events.push({ - kind: "permission.requested", - payload: { - defaultOptionId: options.find((option) => option.kind === "allow_once")?.optionId ?? null, - details: stringifyForDisplay(toolCall?.["rawInput"]), - options, - requestId: input.requestId, - targetItemId: toolCallId, - title, - toolCall: toolCall === null ? null : toToolCallPayload(toolCallId, "running", toolCall), - }, - ...(input.runId === null ? {} : { runId: input.runId }), - }); - return { - defaultOptionId: options.find((option) => option.kind === "allow_once")?.optionId ?? null, events, options, - requestId: input.requestId, + request: { + rawInput: stringifyForDisplay(toolCall?.["rawInput"]), + requestId: input.requestId, + title, + toolCallId: nativeToolCallId, + toolKind: readNonEmptyString(toolCall, "kind"), + }, targetItemId: toolCallId, - title, toolCall, }; } -export function toPermissionResolvedEvent(input: { - option: AcpPermissionOption | null; - requestId: string; - runId: RunId | null; -}): DriverEventInput { - return { - kind: "permission.resolved", - payload: - input.option === null - ? { - outcome: "cancelled", - requestId: input.requestId, - } - : { - optionId: input.option.optionId, - optionKind: input.option.kind, - outcome: "selected", - requestId: input.requestId, - }, - ...(input.runId === null ? {} : { runId: input.runId }), - }; -} - function normalizePermissionOptions(raw: unknown): AcpPermissionOption[] { if (!Array.isArray(raw)) { return []; diff --git a/src/runtimes/acp/acp-session-setup.ts b/src/runtimes/acp/acp-session-setup.ts index 8b2a152..445d602 100644 --- a/src/runtimes/acp/acp-session-setup.ts +++ b/src/runtimes/acp/acp-session-setup.ts @@ -24,7 +24,6 @@ import type { JsonObject } from "./acp-types"; export type AcpSessionSetupMode = "created" | "loaded" | "resumed"; export interface AcpSessionSetup { - readonly droppedAdditionalDirectories: readonly string[]; readonly mode: AcpSessionSetupMode; readonly raw: JsonObject; readonly sessionId: string; @@ -80,16 +79,11 @@ export async function setupAcpSession(input: AcpSessionSetupInput): Promise 0 && !supportsAdditionalDirs(input.agentCapabilities)) { + throw new Error("ACP agent does not advertise additionalDirectories support."); + } const baseParams = { _meta: toRequestMeta({ @@ -120,7 +114,6 @@ export async function setupAcpSession(input: AcpSessionSetupInput): Promise>(); #failure: Error | null = null; readonly #onFailure: AcpSessionUpdateInboxOptions["onFailure"]; #pendingBytes = 0; @@ -89,7 +88,7 @@ export class AcpSessionUpdateInbox { this.#pendingCount += 1; const gate = this.#updateGate; const scope = { replaying: this.#replaying, suppressed: this.#suppressed }; - let delivery = Promise.resolve(); + let admitted = false; const admission = this.#tail .then(async () => { if (this.#failure !== null) { @@ -99,28 +98,33 @@ export class AcpSessionUpdateInbox { return; } - delivery = this.#apply(context, notification, scope); - this.#deliveries.add(delivery); - void delivery - .catch((error: unknown) => this.#fail(error)) - .finally(() => this.#deliveries.delete(delivery)); - }) - .finally(() => { + admitted = true; this.#pendingBytes -= bytes; this.#pendingCount -= 1; + try { + await this.#apply(context, notification, scope); + } catch (error) { + this.#fail(error); + throw error; + } + }) + .finally(() => { + if (!admitted) { + this.#pendingBytes -= bytes; + this.#pendingCount -= 1; + } }); this.#tail = admission.catch((error: unknown) => this.#fail(error)); - return admission.then(() => delivery); + return admission; } async drain(): Promise { for (;;) { const tail = this.#tail; await tail; - await Promise.allSettled(this.#deliveries); await Promise.resolve(); - if (tail !== this.#tail || this.#deliveries.size > 0) { + if (tail !== this.#tail) { continue; } diff --git a/src/runtimes/acp/acp-terminal-manager.ts b/src/runtimes/acp/acp-terminal-manager.ts index b2338b0..390c2ed 100644 --- a/src/runtimes/acp/acp-terminal-manager.ts +++ b/src/runtimes/acp/acp-terminal-manager.ts @@ -43,6 +43,7 @@ interface AcpTerminalState { readonly outputByteLimit: number; readonly process: ChildProcessWithoutNullStreams; readonly rejectExit: (reason?: unknown) => void; + readonly reservation: AcpTerminalReservation; releaseTask: Promise | null; readonly resolveExit: (status: AcpTerminalExitStatus) => void; supervisionFailure: Error | null; @@ -51,6 +52,12 @@ interface AcpTerminalState { watchdog: LinuxProcessTreeWatchdog | null; } +interface AcpTerminalReservation { + active: boolean; + claimed: boolean; + readonly turn: number; +} + interface AcpTerminalExitStatus { readonly exitCode: number | null; readonly signal: string | null; @@ -84,9 +91,11 @@ export class AcpTerminalManager { readonly #maxTerminals: number; readonly #pathScope: AcpPathScope; readonly #push: AcpTerminalManagerOptions["push"]; - readonly #createTasks = new Set>(); + readonly #createTasks = new Map, number>(); readonly #spawnWatchdog: typeof spawnLinuxProcessTreeWatchdog; + #currentTurn = 0; #stopping = false; + #terminalReservations = 0; readonly #terminals = new Map(); constructor(options: AcpTerminalManagerOptions) { @@ -110,20 +119,41 @@ export class AcpTerminalManager { if (this.#stopping) { throw new Error("ACP terminal manager is stopping."); } + if (this.#terminalReservations >= this.#maxTerminals) { + throw new Error(`ACP terminal limit of ${this.#maxTerminals} is exhausted.`); + } - const creation = this.#create(context, params, signal); - this.#createTasks.add(creation); + const reservation: AcpTerminalReservation = { + active: true, + claimed: false, + turn: this.#currentTurn, + }; + this.#terminalReservations += 1; + const creation = this.#create(context, params, reservation, signal); + this.#createTasks.set(creation, reservation.turn); try { return await creation; } finally { this.#createTasks.delete(creation); + if (!reservation.claimed) { + this.#releaseReservation(reservation); + } + } + } + + beginTurn(): number { + if (this.#stopping) { + throw new Error("ACP terminal manager is stopping."); } + + return ++this.#currentTurn; } async #create( context: AgentDriverContext, params: unknown, + reservation: AcpTerminalReservation, signal?: AbortSignal, ): Promise<{ terminalId: string }> { signal?.throwIfAborted(); @@ -139,10 +169,6 @@ export class AcpTerminalManager { throw new Error("ACP terminal/create requires a command."); } - if (this.#terminals.size >= this.#maxTerminals) { - throw new Error(`ACP terminal limit of ${this.#maxTerminals} is exhausted.`); - } - const args = readArray(record, "args").filter( (entry): entry is string => typeof entry === "string", ); @@ -189,6 +215,7 @@ export class AcpTerminalManager { outputByteLimit, process: child, rejectExit, + reservation, releaseTask: null, resolveExit, supervisionFailure: null, @@ -197,6 +224,7 @@ export class AcpTerminalManager { watchdog: null, }; + reservation.claimed = true; this.#terminals.set(terminalId, terminal); child.once("close", (exitCode, signal) => { const status = { exitCode: exitCode ?? null, signal: signal ?? null }; @@ -327,7 +355,7 @@ export class AcpTerminalManager { } releaseLinuxProcessMarker(terminal.marker); - this.#terminals.delete(terminalId); + this.#removeTerminal(terminal); throw error; } @@ -403,9 +431,24 @@ export class AcpTerminalManager { async stopAll(context: AgentDriverContext): Promise { this.#stopping = true; - const creations = await Promise.allSettled(this.#createTasks); + await this.#stop(context); + } + + async stopTurn(context: AgentDriverContext, turn: number): Promise { + await this.#stop(context, turn); + } + + async #stop(context: AgentDriverContext, turn?: number): Promise { + const ownsTurn = (ownedTurn: number): boolean => turn === undefined || ownedTurn === turn; + const creations = await Promise.allSettled( + [...this.#createTasks].flatMap(([creation, ownedTurn]) => + ownsTurn(ownedTurn) ? [creation] : [], + ), + ); const releases = await Promise.allSettled( - [...this.#terminals.values()].map((terminal) => this.#releaseTerminal(context, terminal)), + [...this.#terminals.values()].flatMap((terminal) => + ownsTurn(terminal.reservation.turn) ? [this.#releaseTerminal(context, terminal)] : [], + ), ); const failures = [ ...creations.flatMap((result) => @@ -498,8 +541,26 @@ export class AcpTerminalManager { if (this.#terminals.get(terminal.id) === terminal) { releaseLinuxProcessMarker(terminal.marker); - this.#terminals.delete(terminal.id); + this.#removeTerminal(terminal); + } + } + + #removeTerminal(terminal: AcpTerminalState): void { + if (this.#terminals.get(terminal.id) !== terminal) { + return; + } + + this.#terminals.delete(terminal.id); + this.#releaseReservation(terminal.reservation); + } + + #releaseReservation(reservation: AcpTerminalReservation): void { + if (!reservation.active) { + return; } + + reservation.active = false; + this.#terminalReservations -= 1; } async #completeExit( diff --git a/src/runtimes/acp/acp-tool-events.ts b/src/runtimes/acp/acp-tool-events.ts index 0a42752..76ff306 100644 --- a/src/runtimes/acp/acp-tool-events.ts +++ b/src/runtimes/acp/acp-tool-events.ts @@ -3,9 +3,9 @@ import { isDeepStrictEqual } from "node:util"; import type { DriverEventInput } from "../../protocol/events"; import type { RunId } from "../../protocol/id"; import { + MAX_ACP_LOSSLESS_EVENT_BYTES, isRecord, readNonEmptyString, - readNullableString, readNumber, readRecord, readString, @@ -13,7 +13,7 @@ import { } from "./acp-types"; import type { JsonObject } from "./acp-types"; -export type RuntimeToolStatus = "completed" | "failed" | "running"; +export type RuntimeToolStatus = "cancelled" | "completed" | "failed" | "running"; function readToolDisplayString(value: unknown): string | undefined { const display = stringifyForDisplay(value); @@ -41,10 +41,10 @@ function hasNonzeroExecuteExit(kind: unknown, update: JsonObject | null): boolea } export class AcpToolEventState { - readonly #completed = new Set(); - readonly #nonzeroExecuteExits = new Set(); - readonly #snapshots = new Map(); - readonly #started = new Set(); + #completed = new Set(); + #nonzeroExecuteExits = new Set(); + #snapshots = new Map(); + #started = new Set(); hasActivity(): boolean { return this.#started.size > 0; @@ -61,6 +61,20 @@ export class AcpToolEventState { this.#started.clear(); } + checkpoint(): () => void { + const completed = new Set(this.#completed); + const nonzeroExecuteExits = new Set(this.#nonzeroExecuteExits); + const snapshots = new Map(this.#snapshots); + const started = new Set(this.#started); + + return () => { + this.#completed = completed; + this.#nonzeroExecuteExits = nonzeroExecuteExits; + this.#snapshots = snapshots; + this.#started = started; + }; + } + patch(input: { parentMessageId?: string | undefined; status: RuntimeToolStatus | null; @@ -72,14 +86,15 @@ export class AcpToolEventState { const kind = readNonEmptyString(input.update, "kind") ?? previous?.["kind"] ?? "tool"; const nextStatus = input.status ?? "running"; - if (hasNonzeroExecuteExit(kind, input.update)) { - this.#nonzeroExecuteExits.add(input.toolCallId); - } + const hasNonzeroExit = + this.#nonzeroExecuteExits.has(input.toolCallId) || hasNonzeroExecuteExit(kind, input.update); const status = - previousStatus === "completed" || previousStatus === "failed" + previousStatus === "cancelled" || + previousStatus === "completed" || + previousStatus === "failed" ? previousStatus - : nextStatus === "completed" && this.#nonzeroExecuteExits.has(input.toolCallId) + : nextStatus === "completed" && hasNonzeroExit ? "failed" : nextStatus; // The projection layer links tool calls to their assistant message via @@ -88,17 +103,26 @@ export class AcpToolEventState { (typeof previous?.["parentMessageId"] === "string" ? previous["parentMessageId"] : undefined) ?? input.parentMessageId; + const title = readNonEmptyString(input.update, "title") ?? previous?.["title"]; const payload = { ...previous, ...toToolCallPayload(input.toolCallId, status, input.update), kind, ...(parentMessageId === undefined ? {} : { parentMessageId }), status, - title: readNullableString(input.update, "title") ?? previous?.["title"] ?? null, + ...(typeof title === "string" ? { title } : {}), toolCallId: input.toolCallId, }; const changed = previous === undefined || !isDeepStrictEqual(previous, payload); + if (Buffer.byteLength(JSON.stringify(payload), "utf8") > MAX_ACP_LOSSLESS_EVENT_BYTES) { + throw new RangeError(`ACP tool update exceeds ${MAX_ACP_LOSSLESS_EVENT_BYTES} UTF-8 bytes.`); + } + + if (hasNonzeroExit) { + this.#nonzeroExecuteExits.add(input.toolCallId); + } + if (changed) { this.#snapshots.set(input.toolCallId, structuredClone(payload)); } @@ -123,7 +147,6 @@ export class AcpToolEventState { error: input.status === "failed" ? readString(input.update, "error") : undefined, itemId: input.toolCallId, itemType: "tool_call", - result: input.update?.["rawOutput"], status: input.status, }, runId: input.runId, @@ -146,7 +169,7 @@ export class AcpToolEventState { events.push({ kind: "tool.call.updated", payload: { - ...this.#snapshots.get(itemId), + ...(input.error === undefined ? {} : { error: input.error }), status: input.status, toolCallId: itemId, }, @@ -198,7 +221,7 @@ export function toRuntimeToolStatus(status: string | null): RuntimeToolStatus { return "completed"; } - if (status === "failed" || status === "cancelled") { + if (status === "failed") { return "failed"; } @@ -212,9 +235,10 @@ export function toToolCallPayload( ): JsonObject { const content = readToolContentString(update?.["content"]); const kind = readNonEmptyString(update, "kind"); + const name = readNonEmptyString(update, "name"); const rawInput = readToolDisplayString(update?.["rawInput"]); const rawOutput = readToolDisplayString(update?.["rawOutput"]); - const title = readNullableString(update, "title"); + const title = readNonEmptyString(update, "title"); const locations = update?.["locations"]; return { @@ -222,7 +246,8 @@ export function toToolCallPayload( ...(kind === null ? {} : { kind }), ...(locations === undefined || locations === null ? {} : { locations }), ...(rawInput === undefined ? {} : { rawInput }), - ...(rawOutput === undefined ? {} : { rawOutput }), + ...(rawOutput === undefined || rawOutput === content ? {} : { rawOutput }), + ...(name === null ? {} : { name }), status, ...(title === undefined || title === null ? {} : { title }), toolCallId, diff --git a/src/runtimes/acp/acp-turn-controller.ts b/src/runtimes/acp/acp-turn-controller.ts index cbb62a6..6d1523a 100644 --- a/src/runtimes/acp/acp-turn-controller.ts +++ b/src/runtimes/acp/acp-turn-controller.ts @@ -16,7 +16,8 @@ import type { RuntimeCommandInput } from "../../runtime-command"; import { raceWithAbort } from "../../utils/async"; import type { AcpClientRequestHandler } from "./acp-client-request-handler"; import { toRequestMeta } from "./acp-configuration"; -import { AcpTurnEventState, toPromptStartEvents } from "./acp-event-translator"; +import { AcpAssistantTranscriptState } from "./acp-assistant-transcript-state"; +import { toPromptStartEvents } from "./acp-session-events"; interface ActiveAcpTurn { readonly cancellation: AbortController; @@ -30,6 +31,7 @@ interface ActiveAcpTurn { providerPromptAdmitted: boolean; readonly providerPromptSettled: ReturnType>; readonly runId: RunId; + readonly terminalTurn: number; terminalStarted: boolean; } @@ -86,20 +88,31 @@ export type AcpTurnEventPush = ( events: DriverEventInput[], ) => Promise; +export type AcpTurnTerminalPush = ( + context: AgentDriverContext, + reason: string, + closures: readonly DriverEventInput[], + terminal: DriverEventInput, +) => Promise; + export type AcpCancelledTurnBarrier = (context: AgentDriverContext) => Promise; export class AcpTurnController { #active: ActiveAcpTurn | null = null; readonly #cancelledTurnBarrier: AcpCancelledTurnBarrier; - readonly events = new AcpTurnEventState(); + readonly events = new AcpAssistantTranscriptState(); readonly #push: AcpTurnEventPush; + readonly #pushTerminal: AcpTurnTerminalPush; constructor( push: AcpTurnEventPush, cancelledTurnBarrier: AcpCancelledTurnBarrier = async () => {}, + pushTerminal: AcpTurnTerminalPush = async (context, reason, closures, terminal) => + push(context, reason, [...closures, terminal]), ) { this.#push = push; this.#cancelledTurnBarrier = cancelledTurnBarrier; + this.#pushTerminal = pushTerminal; } isCancelling(): boolean { @@ -155,10 +168,11 @@ export class AcpTurnController { providerPromptAdmitted: false, providerPromptSettled: Promise.withResolvers(), runId, + terminalTurn: clientRequests.beginTurnTerminals(), terminalStarted: false, }; this.#active = active; - this.events.begin({ messageId, runId, sessionId }); + this.events.begin({ messageId, runId }); const onAbort = () => requestCancellation( active, @@ -173,6 +187,7 @@ export class AcpTurnController { clientRequests.openPermissionIngress(); clientRequests.openTurnUpdateIngress(); let drainTask: Promise | null = null; + let preserveEventState = false; const drain = async () => { try { await (drainTask ??= drainTurnWork(clientRequests, active.drainCancellation.signal)); @@ -188,6 +203,26 @@ export class AcpTurnController { await drainTask; } }; + const publishTerminal = async ( + reason: string | ((events: readonly DriverEventInput[]) => string), + prepare: () => DriverEventInput[], + ): Promise => { + const restore = this.events.checkpoint(); + + try { + const events = prepare(); + await this.#pushTerminalEvents( + context, + typeof reason === "string" ? reason : reason(events), + events, + ); + return events; + } catch (error) { + restore(); + preserveEventState = true; + throw error; + } + }; context.logger.info("driver.acp.prompt.sending", { sessionId, @@ -215,9 +250,7 @@ export class AcpTurnController { active.cancellationReason ?? "ACP driver backend turn was cancelled.", ); active.terminalStarted = true; - await this.#push( - context, - "driver.acp.prompt.cancelled", + await publishTerminal("driver.acp.prompt.cancelled", () => this.events.completePrompt("cancelled", null), ); throw new DriverTurnCancelledError("ACP driver backend turn was cancelled."); @@ -242,21 +275,19 @@ export class AcpTurnController { const stopReason = active.cancelRequested ? "cancelled" : promptResult.stopReason; const promptCancelled = active.cancelRequested || promptResult.stopReason === "cancelled"; if (promptCancelled) { - await this.#crossCancelledTurnBarrier(context, active); + await this.#crossCancelledTurnBarrier(context, active, clientRequests); } - const completionEvents = this.events.completePrompt(stopReason, promptResult.usage); - const promptFailed = completionEvents.some((event) => event.kind === "run.failed"); - active.terminalStarted = true; - await this.#push( - context, - promptCancelled - ? "driver.acp.prompt.cancelled" - : promptFailed - ? "driver.acp.prompt.failed" - : "driver.acp.prompt.completed", - completionEvents, + const completionEvents = await publishTerminal( + (events) => + promptCancelled + ? "driver.acp.prompt.cancelled" + : events.some((event) => event.kind === "run.failed") + ? "driver.acp.prompt.failed" + : "driver.acp.prompt.completed", + () => this.events.completePrompt(stopReason, promptResult.usage), ); + const promptFailed = completionEvents.some((event) => event.kind === "run.failed"); context.logger.info( promptFailed ? "driver.acp.prompt.failed" : "driver.acp.prompt.completed", { sessionId, stopReason: promptResult.stopReason }, @@ -273,12 +304,23 @@ export class AcpTurnController { active.providerPromptSettled.resolve(active.providerPromptAdmitted && active.cancelRequested); clientRequests.closePermissionIngress(); clientRequests.closeTurnUpdateIngress(); + if (preserveEventState) { + throw error; + } const fatal = readFatal(active); const fatalCleanup = fatal === null ? null : Promise.allSettled([fatal.cleanup, clientRequests.stopTerminals(context)]); - await drain(); + let catchDrainError: unknown = null; + try { + await drain(); + } catch (drainError) { + catchDrainError = drainError; + context.logger.warn("driver.acp.prompt.drain.failed", { + message: drainError instanceof Error ? drainError.message : "ACP turn drain failed.", + }); + } if (fatal !== null && fatalCleanup !== null) { const cleanupResults = await fatalCleanup; @@ -291,9 +333,7 @@ export class AcpTurnController { } active.terminalStarted = true; - await this.#push( - context, - "driver.acp.provider.failed", + await publishTerminal("driver.acp.provider.failed", () => this.events.failPrompt({ code: "acp.provider_failed", message: fatal.error.message, @@ -302,15 +342,23 @@ export class AcpTurnController { throw fatal.error; } + if (catchDrainError !== null) { + const message = + catchDrainError instanceof Error ? catchDrainError.message : "ACP turn drain failed."; + active.terminalStarted = true; + await publishTerminal("driver.acp.prompt.failed", () => + this.events.failPrompt({ code: "acp.turn_drain_failed", message }), + ); + throw catchDrainError; + } + if (error instanceof DriverTurnCancelledError || error instanceof AcpPromptTerminalError) { throw error; } if (error instanceof DriverTurnCancellationCleanupError) { active.terminalStarted = true; - await this.#push( - context, - "driver.acp.prompt.failed", + await publishTerminal("driver.acp.prompt.failed", () => this.events.failPrompt({ code: "acp.cancel_cleanup_failed", message: error.message, @@ -321,12 +369,10 @@ export class AcpTurnController { if (active.cancelRequested) { try { - await this.#crossCancelledTurnBarrier(context, active); + await this.#crossCancelledTurnBarrier(context, active, clientRequests); } catch (cleanupError) { active.terminalStarted = true; - await this.#push( - context, - "driver.acp.prompt.failed", + await publishTerminal("driver.acp.prompt.failed", () => this.events.failPrompt({ code: "acp.cancel_cleanup_failed", message: @@ -337,18 +383,16 @@ export class AcpTurnController { ); throw cleanupError; } - const events = - this.events.activeRunId() === null ? [] : this.events.completePrompt("cancelled", null); active.terminalStarted = true; - await this.#push(context, "driver.acp.prompt.cancelled", events); + await publishTerminal("driver.acp.prompt.cancelled", () => + this.events.activeRunId() === null ? [] : this.events.completePrompt("cancelled", null), + ); throw new DriverTurnCancelledError("ACP driver backend turn was cancelled."); } const message = error instanceof Error ? error.message : "ACP driver backend turn failed."; active.terminalStarted = true; - await this.#push( - context, - "driver.acp.prompt.failed", + await publishTerminal("driver.acp.prompt.failed", () => this.events.failPrompt({ code: "acp.turn_failed", message }), ); throw error; @@ -361,7 +405,9 @@ export class AcpTurnController { signal?.removeEventListener("abort", onAbort); active.providerPromptSettled.resolve(false); this.#active = null; - this.events.clear(); + if (!preserveEventState) { + this.events.clear(); + } } } @@ -408,16 +454,41 @@ export class AcpTurnController { ])); } - async #crossCancelledTurnBarrier( + async #pushTerminalEvents( context: AgentDriverContext, - active: ActiveAcpTurn, + reason: string, + events: DriverEventInput[], ): Promise { - if (!active.providerPromptAdmitted) { + if (events.length === 0) { + await this.#push(context, reason, events); return; } + const terminal = events.at(-1)!; + + if ( + terminal.kind !== "run.cancelled" && + terminal.kind !== "run.completed" && + terminal.kind !== "run.failed" + ) { + throw new Error("ACP terminal event batch must end with a run terminal."); + } + + await this.#pushTerminal(context, reason, events.slice(0, -1), terminal); + } + + async #crossCancelledTurnBarrier( + context: AgentDriverContext, + active: ActiveAcpTurn, + clientRequests: AcpClientRequestHandler, + ): Promise { try { - await (active.cancellationBarrier ??= this.#cancelledTurnBarrier(context)); + await (active.cancellationBarrier ??= (async () => { + await clientRequests.stopTurnTerminals(context, active.terminalTurn); + if (active.providerPromptAdmitted) { + await this.#cancelledTurnBarrier(context); + } + })()); } catch (error) { throw new DriverTurnCancellationCleanupError( `ACP cancelled turn process recycle failed: ${ diff --git a/src/runtimes/acp/acp-types.ts b/src/runtimes/acp/acp-types.ts index 2fcd543..a496c18 100644 --- a/src/runtimes/acp/acp-types.ts +++ b/src/runtimes/acp/acp-types.ts @@ -1,11 +1,33 @@ +import type { DriverEventInput } from "../../protocol/events"; import type { JsonObject } from "../provider-json"; +export const MAX_ACP_LOSSLESS_EVENT_BYTES = 512 * 1_024; + +// OpenCode reports cache read/write as separate Anthropic-style buckets. +export const ACP_USAGE_CONTRACT = "anthropic_bucketed"; + +export function assertBoundedLosslessEvents(events: DriverEventInput[]): DriverEventInput[] { + for (const event of events) { + if ( + event.delivery !== "best_effort" && + Buffer.byteLength(JSON.stringify(event), "utf8") > MAX_ACP_LOSSLESS_EVENT_BYTES + ) { + throw new RangeError( + `ACP ${event.kind} event exceeds ${MAX_ACP_LOSSLESS_EVENT_BYTES} UTF-8 bytes.`, + ); + } + } + + return events; +} + export { raceWithAbort } from "../../utils/async"; export { isRecord, readArray, readNonEmptyString, + readNumber, readRecord, readString, stringifyForDisplay, @@ -24,8 +46,3 @@ export function readNullableString( return typeof entry === "string" ? entry : undefined; } - -export function readNumber(value: JsonObject | null, key: string): number | null { - const entry = value?.[key]; - return typeof entry === "number" && Number.isFinite(entry) ? entry : null; -} diff --git a/src/runtimes/acp/contract-item-projector.ts b/src/runtimes/acp/contract-item-projector.ts deleted file mode 100644 index f20bbe0..0000000 --- a/src/runtimes/acp/contract-item-projector.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import type { SessionUpdate, ToolCall, ToolCallUpdate } from "@agentclientprotocol/sdk"; - -import { itemSchema, toolItemSchema } from "../../contract"; -import type { FileChange, ItemStatus, ToolItem } from "../../contract"; -import { - asJsonValue, - createProviderMeta, - ContractProjection, - nonEmpty, -} from "../contract-projection"; -import type { AcpContractTerminalProjector } from "./contract-terminal-projector"; -import { - itemStatus, - toChanges, - toContentBlocks, - toolCategory, - toolError, - toOutput, -} from "./contract-mapping"; - -const { cause: providerCause, provenance } = createProviderMeta("agent-client-protocol"); - -export interface AcpContractItemProjectorOptions { - readonly now: () => string; - readonly projection: ContractProjection; - readonly resolveId: (runId: string, kind: string, nativeId: string) => string; - readonly terminals: AcpContractTerminalProjector; -} - -export class AcpContractItemProjector { - readonly #now: () => string; - readonly #projection: ContractProjection; - readonly #resolveId: AcpContractItemProjectorOptions["resolveId"]; - readonly #terminals: AcpContractTerminalProjector; - - constructor(options: AcpContractItemProjectorOptions) { - this.#now = options.now; - this.#projection = options.projection; - this.#resolveId = options.resolveId; - this.#terminals = options.terminals; - } - - async putMessageChunk( - runId: string, - update: Extract< - SessionUpdate, - { sessionUpdate: "agent_message_chunk" | "agent_thought_chunk" } - >, - kind: "message" | "reasoning", - ): Promise { - const nativeId = update.messageId ?? `${runId}:anonymous:${kind}`; - const id = this.#resolveId(runId, kind, nativeId); - const event = `session/${update.sessionUpdate}`; - let item = this.#projection.item(runId, id); - - if (item === undefined) { - const now = this.#now(); - item = await this.#projection.putItem( - runId, - event, - providerCause(event, nativeId), - itemSchema.parse({ - audience: "participants", - content: [], - createdAt: now, - id, - kind, - ...(kind === "message" ? { phase: "final", role: "agent" } : {}), - provenance: provenance( - event, - update.messageId === null || update.messageId === undefined - ? undefined - : { messageId: update.messageId }, - ), - runId, - status: "active", - updatedAt: now, - }), - ); - } - - if (item.status !== "active" || item.kind !== kind) { - return; - } - - const channel = kind === "message" ? "message.text" : "reasoning.text"; - - if (update.content.type === "text") { - await this.#projection.appendText({ - cause: providerCause(event, nativeId), - channel, - delta: update.content.text, - event, - itemId: id, - runId, - }); - return; - } - - const checkpoint = await this.#projection.checkpointText({ - cause: providerCause(`${event}.checkpoint`, nativeId), - channel, - event: `${event}.checkpoint`, - itemId: id, - runId, - }); - const current = checkpoint ?? item; - - if (current.kind !== "message" && current.kind !== "reasoning") { - throw new Error("ACP v1 message chunk changed item kind while being projected."); - } - - await this.#projection.putItem( - runId, - event, - providerCause(event, nativeId), - itemSchema.parse({ - ...current, - content: [...current.content, ...toContentBlocks(update.content)], - updatedAt: this.#now(), - }), - ); - } - - async putTool( - runId: string, - update: ToolCall | ToolCallUpdate, - event: string, - ): Promise { - const id = this.#resolveId(runId, "tool", update.toolCallId); - const existing = this.#projection.item(runId, id); - - if (existing !== undefined && existing.kind !== "tool") { - throw new Error("ACP v1 tool update collided with a non-tool item."); - } - - const existingTool = existing?.kind === "tool" ? existing : undefined; - const now = this.#now(); - const title = nonEmpty(update.title, existingTool?.title ?? existingTool?.name ?? "Tool"); - const nextStatus = - update.status === undefined || update.status === null - ? (existingTool?.status ?? "active") - : itemStatus(update.status); - const status = - existingTool === undefined || existingTool.status === "active" - ? nextStatus - : existingTool.status; - const content = update.content ?? undefined; - const terminalIds = - content?.flatMap((entry) => (entry.type === "terminal" ? [entry.terminalId] : [])) ?? []; - const projectedTerminalIds: string[] = []; - - for (const terminalId of terminalIds) { - projectedTerminalIds.push(await this.#terminals.ensureTerminal(runId, terminalId)); - } - - const terminalItemId = - content === undefined ? existingTool?.terminalItemId : projectedTerminalIds[0]; - - const input = - update.rawInput === undefined || update.rawInput === null - ? existingTool?.input - : asJsonValue(update.rawInput); - const structuredOutput = - update.rawOutput === undefined || update.rawOutput === null - ? existingTool?.structuredOutput - : asJsonValue(update.rawOutput); - const output = content === undefined ? existingTool?.output : toOutput(content); - const locations = - update.locations === undefined || update.locations === null - ? existingTool?.locations - : update.locations.flatMap((location) => - location.path.trim().length === 0 - ? [] - : [ - { - ...(location.line === undefined || - location.line === null || - !Number.isSafeInteger(location.line) || - location.line < 1 - ? {} - : { line: location.line }), - path: location.path, - }, - ], - ); - const item = toolItemSchema.parse({ - audience: "participants", - category: - update.kind === undefined || update.kind === null - ? (existingTool?.category ?? "other") - : toolCategory(update.kind), - createdAt: existingTool?.createdAt ?? now, - ...(status === "active" ? {} : { endedAt: existingTool?.endedAt ?? now }), - ...(status === "failed" ? { error: existingTool?.error ?? toolError(title) } : {}), - id, - ...(input === undefined ? {} : { input }), - kind: "tool", - ...(locations === undefined ? {} : { locations }), - name: existingTool?.name ?? title, - origin: "provider", - ...(output === undefined ? {} : { output }), - provenance: provenance(event, { toolCallId: update.toolCallId }), - runId, - status, - ...(structuredOutput === undefined ? {} : { structuredOutput }), - ...(terminalItemId === undefined ? {} : { terminalItemId }), - title, - updatedAt: now, - }); - const changed = - existingTool === undefined || - !isDeepStrictEqual( - { ...existingTool, provenance: item.provenance, updatedAt: item.updatedAt }, - item, - ); - - if (changed) { - await this.#projection.putItem(runId, event, providerCause(event, update.toolCallId), item); - } - - await this.#putChanges( - runId, - update.toolCallId, - content === undefined ? undefined : toChanges(content), - status, - event, - now, - ); - - if (changed && item.status === "active") { - await this.#projection.replacePreview({ - channel: "tool.progress", - itemId: item.id, - runId, - text: item.title ?? item.name, - }); - } - - return changed ? item : existingTool; - } - - async #putChanges( - runId: string, - toolCallId: string, - changes: FileChange[] | undefined, - status: ItemStatus, - event: string, - now: string, - ): Promise { - const id = this.#resolveId(runId, "change", toolCallId); - const existing = this.#projection.item(runId, id); - - if (existing !== undefined && existing.kind !== "change") { - return; - } - - const nextStatus = - existing === undefined || existing.status === "active" ? status : existing.status; - - if ( - (changes === undefined && (existing === undefined || existing.status === nextStatus)) || - (changes?.length === 0 && existing === undefined) - ) { - return; - } - - const item = itemSchema.parse({ - audience: "participants", - changes: changes ?? existing?.changes, - createdAt: existing?.createdAt ?? now, - ...(nextStatus === "active" ? {} : { endedAt: existing?.endedAt ?? now }), - ...(nextStatus === "failed" ? { error: existing?.error ?? toolError("File change") } : {}), - id, - kind: "change", - provenance: provenance(event, { toolCallId }), - runId, - status: nextStatus, - updatedAt: now, - }); - - if ( - existing !== undefined && - isDeepStrictEqual( - { ...existing, provenance: item.provenance, updatedAt: item.updatedAt }, - item, - ) - ) { - return; - } - - await this.#projection.putItem( - runId, - `${event}.changes`, - providerCause(`${event}.changes`, toolCallId), - item, - ); - } - - async putPlan( - runId: string, - nativeId: string, - entries: Extract["entries"], - event: string, - ): Promise { - const id = this.#resolveId(runId, "plan", nativeId); - const existing = this.#projection.item(runId, id); - - if (existing !== undefined && (existing.kind !== "plan" || existing.status !== "active")) { - return; - } - - const now = this.#now(); - await this.#projection.putItem( - runId, - event, - providerCause(event, nativeId), - itemSchema.parse({ - audience: "participants", - createdAt: existing?.createdAt ?? now, - entries: entries.map((entry) => ({ - priority: entry.priority, - status: entry.status, - text: entry.content, - })), - id, - kind: "plan", - provenance: provenance(event, nativeId === "current" ? undefined : { planId: nativeId }), - runId, - status: "active", - updatedAt: now, - }), - ); - } -} diff --git a/src/runtimes/acp/contract-mapping.ts b/src/runtimes/acp/contract-mapping.ts deleted file mode 100644 index 14fbc6e..0000000 --- a/src/runtimes/acp/contract-mapping.ts +++ /dev/null @@ -1,219 +0,0 @@ -import type { - ContentBlock as AcpContentBlock, - SessionConfigOption, - ToolCall, - ToolCallContent, - ToolKind, - Usage, -} from "@agentclientprotocol/sdk"; - -import { configOptionSchema } from "../../contract"; -import type { - ConfigOption, - ContentBlock, - FileChange, - ItemStatus, - ProtocolError, - TokenUsage, - ToolItem, -} from "../../contract"; -import { asJsonValue, nonEmpty } from "../contract-projection"; - -const SELECT_GROUPS_EXTENSION = "agentclientprotocol.v1/select-groups"; - -function resourceName(uri: string): string { - try { - const path = new URL(uri).pathname; - return decodeURIComponent(path.split("/").filter(Boolean).at(-1) ?? "resource").slice(0, 1_024); - } catch { - return "resource"; - } -} - -export function toContentBlocks(block: AcpContentBlock): ContentBlock[] { - switch (block.type) { - case "text": - return [{ text: block.text, type: "text" }]; - case "image": - case "audio": - return [{ data: block.data, mediaType: block.mimeType, type: "inline_blob" }]; - case "resource_link": { - if (URL.canParse(block.uri)) { - return [ - { - ...(block.mimeType === undefined || block.mimeType === null - ? {} - : { mediaType: block.mimeType }), - name: nonEmpty(block.name, resourceName(block.uri)), - type: "resource_link", - uri: block.uri, - }, - ]; - } - - const value = asJsonValue(block); - return value === undefined ? [] : [{ type: "json", value }]; - } - case "resource": { - const resource = block.resource; - - if ("text" in resource) { - const content: ContentBlock[] = [{ text: resource.text, type: "text" }]; - - if (URL.canParse(resource.uri)) { - content.unshift({ - ...(resource.mimeType === undefined || resource.mimeType === null - ? {} - : { mediaType: resource.mimeType }), - name: resourceName(resource.uri), - type: "resource_link", - uri: resource.uri, - }); - } - - return content; - } - - return [ - { - data: resource.blob, - mediaType: resource.mimeType ?? "application/octet-stream", - name: resourceName(resource.uri), - type: "inline_blob", - }, - ]; - } - } -} - -export function toolCategory(kind: ToolKind | null | undefined): ToolItem["category"] { - switch (kind) { - case "read": - return "read"; - case "edit": - case "delete": - case "move": - return "edit"; - case "search": - return "search"; - case "execute": - return "execute"; - case "fetch": - return "fetch"; - default: - return "other"; - } -} - -export function itemStatus(status: ToolCall["status"] | null | undefined): ItemStatus { - return status === "completed" ? "completed" : status === "failed" ? "failed" : "active"; -} - -export function toolError(title: string): ProtocolError { - return { - code: "agent_client_protocol.tool_failed", - message: `${title} failed.`, - retryable: false, - }; -} - -export function toOutput(content: readonly ToolCallContent[]): ContentBlock[] { - return content.flatMap((entry) => - entry.type === "content" ? toContentBlocks(entry.content) : [], - ); -} - -export function toChanges(content: readonly ToolCallContent[]): FileChange[] { - return content.flatMap((entry) => { - if (entry.type !== "diff" || entry.path.trim().length === 0) { - return []; - } - - return [ - { - diff: { - type: "json", - value: { - newText: entry.newText, - oldText: entry.oldText ?? null, - }, - }, - operation: entry.oldText === undefined || entry.oldText === null ? "create" : "update", - path: entry.path, - }, - ]; - }); -} - -export function toUsage(usage: Usage, previous: TokenUsage | undefined): TokenUsage { - const input = usage.inputTokens; - const output = usage.outputTokens; - const cachedInput = usage.cachedReadTokens; - const reasoning = usage.thoughtTokens; - const total = usage.totalTokens; - - return { - ...previous, - ...(cachedInput !== undefined && - cachedInput !== null && - Number.isSafeInteger(cachedInput) && - cachedInput >= (previous?.cachedInput ?? 0) - ? { cachedInput } - : {}), - ...(Number.isSafeInteger(input) && input >= (previous?.input ?? 0) ? { input } : {}), - ...(Number.isSafeInteger(output) && output >= (previous?.output ?? 0) ? { output } : {}), - ...(reasoning !== undefined && - reasoning !== null && - Number.isSafeInteger(reasoning) && - reasoning >= (previous?.reasoning ?? 0) - ? { reasoning } - : {}), - ...(Number.isSafeInteger(total) && total >= (previous?.total ?? 0) ? { total } : {}), - }; -} - -function configDescription(value: string | null | undefined) { - return value === undefined || value === null ? {} : { description: value }; -} - -export function toConfigOptions(options: readonly SessionConfigOption[]): ConfigOption[] { - return options.map((option) => { - const base = { - ...(option.category === undefined || option.category === null || option.category.length === 0 - ? {} - : { category: option.category }), - ...configDescription(option.description), - id: option.id, - label: nonEmpty(option.name, option.id), - }; - - if (option.type === "boolean") { - return configOptionSchema.parse({ ...base, type: "boolean", value: option.currentValue }); - } - - const groups = option.options.flatMap((entry) => ("group" in entry ? [entry] : [])); - const choices = option.options.flatMap((entry) => ("group" in entry ? entry.options : [entry])); - - return configOptionSchema.parse({ - ...base, - choices: choices.map((choice) => ({ - ...configDescription(choice.description), - id: choice.value, - label: nonEmpty(choice.name, choice.value), - })), - ...(groups.length === 0 - ? {} - : { - extensions: { - [SELECT_GROUPS_EXTENSION]: groups.map((group) => ({ - id: group.group, - label: group.name, - optionIds: group.options.map((choice) => choice.value), - })), - }, - }), - type: "select", - value: option.currentValue, - }); - }); -} diff --git a/src/runtimes/acp/contract-permission-controller.ts b/src/runtimes/acp/contract-permission-controller.ts deleted file mode 100644 index a318ee3..0000000 --- a/src/runtimes/acp/contract-permission-controller.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import type { RequestPermissionRequest, RequestPermissionResponse } from "@agentclientprotocol/sdk"; - -import { interactionSchema } from "../../contract"; -import type { Interaction, InteractionResolution } from "../../contract"; -import { - AuthorityOutcomeUnknownError, - createProviderMeta, - ContractProjection, - nonEmpty, -} from "../contract-projection"; -import type { AcpContractSessionUpdateInbox } from "./contract-session-update-inbox"; -import type { AcpContractItemProjector } from "./contract-item-projector"; - -const { cause: providerCause, provenance } = createProviderMeta("agent-client-protocol"); - -interface PermissionIntent { - readonly bytes: number; - released: boolean; - readonly receivedAt: string; - readonly request: RequestPermissionRequest; - readonly runId: string; -} - -interface PendingPermission { - readonly intent: PermissionIntent; - readonly interaction: Interaction; - opened: boolean; - readonly optionIds: ReadonlyMap; - response?: RequestPermissionResponse; - readonly toolCallId: string; -} - -interface OpeningPermission { - readonly intent: PermissionIntent; - readonly promise: Promise; -} - -interface UnknownPermission { - readonly error: AuthorityOutcomeUnknownError; - readonly intent: PermissionIntent; - retry?: Promise; -} - -export interface AcpContractPermissionControllerOptions { - readonly assertNativeSession: (sessionId: string) => void; - readonly createId: () => string; - readonly inbox: AcpContractSessionUpdateInbox; - readonly interactionTimeoutMs: number; - readonly items: AcpContractItemProjector; - readonly maxPendingPermissionBytes: number; - readonly now: () => string; - readonly projection: ContractProjection; - readonly resolveId: (runId: string, kind: string, nativeId: string) => string; - readonly withReceiptTime: (receivedAt: string, operation: () => Promise) => Promise; -} - -export class AcpContractPermissionController { - readonly #assertNativeSession: (sessionId: string) => void; - readonly #createId: () => string; - #disposed = false; - readonly #inbox: AcpContractSessionUpdateInbox; - readonly #interactionTimeoutMs: number; - readonly #items: AcpContractItemProjector; - readonly #maxPendingPermissionBytes: number; - readonly #now: () => string; - readonly #openingPermissions = new Map(); - readonly #pendingPermissions = new Map(); - #pendingPermissionBytes = 0; - readonly #projection: ContractProjection; - readonly #resolveId: AcpContractPermissionControllerOptions["resolveId"]; - readonly #textEncoder = new TextEncoder(); - #unknownPermission: UnknownPermission | undefined; - readonly #withReceiptTime: AcpContractPermissionControllerOptions["withReceiptTime"]; - - constructor(options: AcpContractPermissionControllerOptions) { - this.#assertNativeSession = options.assertNativeSession; - this.#createId = options.createId; - this.#inbox = options.inbox; - this.#interactionTimeoutMs = options.interactionTimeoutMs; - this.#items = options.items; - this.#maxPendingPermissionBytes = options.maxPendingPermissionBytes; - this.#now = options.now; - this.#projection = options.projection; - this.#resolveId = options.resolveId; - this.#withReceiptTime = options.withReceiptTime; - } - - async openPermission(runId: string, request: RequestPermissionRequest): Promise { - this.#assertActive(); - const snapshot = structuredClone(request); - const unknownAtAdmission = this.#unknownPermission; - const exactRetry = - unknownAtAdmission !== undefined && - unknownAtAdmission.intent.runId === runId && - isDeepStrictEqual(unknownAtAdmission.intent.request, snapshot) - ? unknownAtAdmission - : undefined; - - if (unknownAtAdmission !== undefined && exactRetry === undefined) { - throw unknownAtAdmission.error; - } - - if (exactRetry?.retry !== undefined) { - return exactRetry.retry; - } - - const toolCallId = snapshot.toolCall.toolCallId; - const openingAtAdmission = this.#openingPermissions.get(toolCallId); - - if (openingAtAdmission !== undefined) { - if ( - openingAtAdmission.intent.runId !== runId || - !isDeepStrictEqual(openingAtAdmission.intent.request, snapshot) - ) { - throw new Error(`ACP v1 permission request ${toolCallId} changed identity or content.`); - } - - return openingAtAdmission.promise; - } - - const existing = [...this.#pendingPermissions].find( - ([, candidate]) => candidate.toolCallId === toolCallId, - ); - - if (existing !== undefined) { - const [interactionId, pending] = existing; - - if (pending.intent.runId !== runId || !isDeepStrictEqual(pending.intent.request, snapshot)) { - throw new Error(`ACP v1 permission request ${toolCallId} changed identity or content.`); - } - - if (pending.opened) { - return interactionId; - } - } - - let intent = exactRetry?.intent; - - if (intent === undefined) { - const bytes = this.#textEncoder.encode(JSON.stringify(snapshot)).byteLength; - - if (bytes > this.#maxPendingPermissionBytes - this.#pendingPermissionBytes) { - throw new RangeError("ACP v1 pending permission budget is exhausted."); - } - - this.#pendingPermissionBytes += bytes; - intent = { - bytes, - receivedAt: this.#projection.now().toISOString(), - released: false, - request: snapshot, - runId, - }; - } - - const stableIntent = intent; - const opening = this.#inbox.enqueue(async () => { - try { - const unknown = this.#unknownPermission; - - if (unknown !== undefined && unknown !== unknownAtAdmission) { - throw unknown.error; - } - - const interactionId = await this.#withReceiptTime(stableIntent.receivedAt, () => - this.#openPermission(stableIntent), - ); - - if (this.#unknownPermission === unknownAtAdmission) { - this.#unknownPermission = undefined; - } - - return interactionId; - } catch (error) { - if (error instanceof AuthorityOutcomeUnknownError) { - this.#unknownPermission ??= { error, intent: stableIntent }; - } else if (this.#unknownPermission?.intent === stableIntent) { - this.#unknownPermission = undefined; - } - - throw error; - } finally { - if (!this.#permissionRetained(stableIntent)) { - this.#releasePermissionIntent(stableIntent); - } - } - }); - - const tracked = opening.finally(() => { - if (this.#openingPermissions.get(toolCallId)?.promise === tracked) { - this.#openingPermissions.delete(toolCallId); - } - - if (exactRetry !== undefined && exactRetry.retry === tracked) { - delete exactRetry.retry; - } - }); - this.#openingPermissions.set(toolCallId, { intent: stableIntent, promise: tracked }); - if (exactRetry !== undefined) { - exactRetry.retry = tracked; - } - - return tracked; - } - - async #openPermission(intent: PermissionIntent): Promise { - const { request, runId } = intent; - let pending: PendingPermission | undefined; - - try { - this.#assertActive(); - this.#assertNativeSession(request.sessionId); - - const existing = [...this.#pendingPermissions].find( - ([, candidate]) => candidate.toolCallId === request.toolCall.toolCallId, - ); - - if (existing !== undefined) { - const [interactionId, existingPermission] = existing; - - if ( - existingPermission.intent.runId !== runId || - !isDeepStrictEqual(existingPermission.intent.request, request) - ) { - throw new Error( - `ACP v1 permission request ${request.toolCall.toolCallId} changed identity or content.`, - ); - } - - if (!existingPermission.opened) { - try { - await this.#projection.putInteraction( - runId, - "permission/requested", - providerCause("permission/requested", existingPermission.toolCallId), - existingPermission.interaction, - ); - existingPermission.opened = true; - } catch (error) { - if (!(error instanceof AuthorityOutcomeUnknownError)) { - this.#dropPermission(interactionId); - } - - throw error; - } - } - - if (existingPermission.response !== undefined) { - this.#projection.releaseInteraction(interactionId); - this.#dropPermission(interactionId); - } - - return interactionId; - } - - if (request.options.length === 0) { - throw new Error("ACP v1 permission request must advertise at least one option."); - } - - const item = await this.#items.putTool(runId, request.toolCall, "permission/requested.tool"); - const createdAt = this.#now(); - const optionIds = new Map(); - const options = request.options.map((option) => { - const id = this.#resolveId(runId, "permission-option", option.optionId); - optionIds.set(id, option.optionId); - return { - effect: option.kind.startsWith("allow") ? "allow" : "deny", - id, - label: nonEmpty(option.name, option.optionId), - scope: option.kind.endsWith("always") ? "session" : "once", - }; - }); - const interaction = interactionSchema.parse({ - audience: "participants", - blocking: true, - createdAt, - expiresAt: new Date(Date.parse(createdAt) + this.#interactionTimeoutMs).toISOString(), - id: this.#createId(), - itemId: item.id, - kind: "permission", - provenance: provenance("permission/requested", { toolCallId: request.toolCall.toolCallId }), - request: { - options, - subject: { itemId: item.id, type: "item" }, - title: nonEmpty(request.toolCall.title, `Allow ${item.name}?`), - }, - runId, - status: "open", - }); - pending = { - intent, - interaction, - opened: false, - optionIds, - toolCallId: request.toolCall.toolCallId, - }; - this.#pendingPermissions.set(interaction.id, pending); - await this.#projection.putInteraction( - runId, - "permission/requested", - providerCause("permission/requested", request.toolCall.toolCallId), - interaction, - ); - pending.opened = true; - return interaction.id; - } catch (error) { - if (pending !== undefined && !(error instanceof AuthorityOutcomeUnknownError)) { - this.#dropPermission(pending.interaction.id); - } - throw error; - } - } - - async resolveInteraction( - interactionId: string, - resolution: InteractionResolution, - ): Promise { - this.#assertActive(); - return this.#inbox.enqueue(() => this.#resolveInteraction(interactionId, resolution)); - } - - async #resolveInteraction( - interactionId: string, - resolution: InteractionResolution, - ): Promise { - const pending = this.#pendingPermissions.get(interactionId); - - if (pending === undefined) { - return null; - } - - if (resolution.kind !== "permission") { - throw new Error("ACP v1 permission interaction requires a permission resolution."); - } - - if (pending.response !== undefined) { - return pending.response; - } - - const selected = resolution.value.type === "selected" ? resolution.value.optionId : null; - const nativeOptionId = selected === null ? undefined : pending.optionIds.get(selected); - - if (selected !== null && nativeOptionId === undefined) { - throw new Error("ACP v1 permission resolution selected an unavailable option."); - } - - const response: RequestPermissionResponse = - nativeOptionId === undefined - ? { outcome: { outcome: "cancelled" } } - : { outcome: { optionId: nativeOptionId, outcome: "selected" } }; - - this.#projection.releaseInteraction(interactionId); - if (this.#unknownPermission?.intent === pending.intent) { - pending.response = response; - } else { - this.#dropPermission(interactionId); - } - - return response; - } - - #dropPermission(interactionId: string): void { - const pending = this.#pendingPermissions.get(interactionId); - - if (pending !== undefined) { - this.#pendingPermissions.delete(interactionId); - this.#releasePermissionIntent(pending.intent); - } - } - - #permissionRetained(intent: PermissionIntent): boolean { - return ( - this.#unknownPermission?.intent === intent || - [...this.#pendingPermissions.values()].some((pending) => pending.intent === intent) - ); - } - - #releasePermissionIntent(intent: PermissionIntent): void { - if (!intent.released) { - intent.released = true; - this.#pendingPermissionBytes -= intent.bytes; - } - } - - releaseRun(runId: string): void { - for (const [id, pending] of this.#pendingPermissions) { - if (pending.intent.runId === runId) { - this.#dropPermission(id); - } - } - } - - dispose(): void { - this.#disposed = true; - this.#openingPermissions.clear(); - for (const id of this.#pendingPermissions.keys()) { - this.#dropPermission(id); - } - if (this.#unknownPermission !== undefined) { - this.#releasePermissionIntent(this.#unknownPermission.intent); - } - this.#unknownPermission = undefined; - } - - #assertActive(): void { - if (this.#disposed) { - throw new Error("ACP v1 permission controller is disposed."); - } - } -} diff --git a/src/runtimes/acp/contract-session-update-inbox.ts b/src/runtimes/acp/contract-session-update-inbox.ts deleted file mode 100644 index 6aecd50..0000000 --- a/src/runtimes/acp/contract-session-update-inbox.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import type { SessionNotification, SessionUpdate } from "@agentclientprotocol/sdk"; - -import { AuthorityOutcomeUnknownError } from "../contract-projection"; - -const MAX_PENDING_UPDATE_BYTES = 32 * 1_024 * 1_024; -const MAX_PENDING_UPDATES = 1_024; - -interface UnknownSessionUpdate { - readonly error: AuthorityOutcomeUnknownError; - readonly notification: SessionNotification; - readonly receivedAt: string; - retry?: Promise; - readonly runId: string; -} - -export interface AcpContractSessionUpdateInboxOptions { - readonly apply: ( - runId: string, - notification: SessionNotification, - receivedAt: string, - ) => Promise; - readonly now: () => Date; -} - -export class AcpContractSessionUpdateInbox { - readonly #apply: AcpContractSessionUpdateInboxOptions["apply"]; - #closed = false; - #failure: Error | null = null; - #mutationTail: Promise = Promise.resolve(); - readonly #now: () => Date; - #pendingBytes = 0; - #pendingCount = 0; - readonly #textEncoder = new TextEncoder(); - #unknown: UnknownSessionUpdate | undefined; - - constructor(options: AcpContractSessionUpdateInboxOptions) { - this.#apply = options.apply; - this.#now = options.now; - } - - async handle(runId: string, notification: SessionNotification): Promise { - this.#assertOpen(); - this.throwIfFailed(); - - const snapshot = structuredClone(notification); - const unknownAtAdmission = this.#unknown; - const exactRetry = - unknownAtAdmission !== undefined && - unknownAtAdmission.runId === runId && - isDeepStrictEqual(unknownAtAdmission.notification, snapshot) - ? unknownAtAdmission - : undefined; - - if (exactRetry?.retry !== undefined) { - return exactRetry.retry; - } - - const bytes = this.#textEncoder.encode(JSON.stringify(snapshot)).byteLength; - if ( - this.#pendingCount >= MAX_PENDING_UPDATES || - bytes > MAX_PENDING_UPDATE_BYTES - this.#pendingBytes - ) { - throw (this.#failure = new Error("ACP v1 session update queue limit exceeded.")); - } - - const receivedAt = this.#now().toISOString(); - this.#pendingBytes += bytes; - this.#pendingCount += 1; - const update = this.enqueue(async () => { - this.throwIfFailed(); - const unknown = this.#unknown; - const retrying = - unknown !== undefined && - unknownAtAdmission === unknown && - unknown.runId === runId && - isDeepStrictEqual(unknown.notification, snapshot); - - try { - if (unknown !== undefined && unknownAtAdmission !== unknown) { - throw unknown.error; - } - - const result = await this.#apply( - runId, - snapshot, - retrying ? unknown.receivedAt : receivedAt, - ); - if (retrying) { - this.#unknown = undefined; - } - return result; - } catch (cause) { - if (cause instanceof AuthorityOutcomeUnknownError) { - this.#unknown ??= { - error: cause, - notification: snapshot, - receivedAt, - runId, - }; - throw cause; - } - - if (retrying) { - this.#unknown = undefined; - } - this.#failure ??= - cause instanceof Error ? cause : new Error("ACP v1 session update failed.", { cause }); - throw this.#failure; - } - }).finally(() => { - this.#pendingBytes -= bytes; - this.#pendingCount -= 1; - }); - - if (exactRetry === undefined) { - return update; - } - - const retry = update.finally(() => { - if (exactRetry.retry === retry) { - delete exactRetry.retry; - } - }); - exactRetry.retry = retry; - return retry; - } - - enqueue(operation: () => Promise): Promise { - this.#assertOpen(); - const mutation = this.#mutationTail.then(operation); - this.#mutationTail = mutation.then( - () => {}, - () => {}, - ); - return mutation; - } - - throwIfFailed(): void { - if (this.#failure !== null) { - throw this.#failure; - } - } - - close(): void { - this.#closed = true; - this.#unknown = undefined; - } - - #assertOpen(): void { - if (this.#closed) { - throw new Error("ACP v1 session update inbox is closed."); - } - } -} diff --git a/src/runtimes/acp/contract-terminal-projector.ts b/src/runtimes/acp/contract-terminal-projector.ts deleted file mode 100644 index c0eca6c..0000000 --- a/src/runtimes/acp/contract-terminal-projector.ts +++ /dev/null @@ -1,378 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import type { - CreateTerminalRequest, - TerminalOutputResponse, - WaitForTerminalExitResponse, -} from "@agentclientprotocol/sdk"; - -import { itemSchema } from "../../contract"; -import type { Item } from "../../contract"; -import { - AuthorityOutcomeUnknownError, - createProviderMeta, - ContractProjection, -} from "../contract-projection"; -import type { AcpContractSessionUpdateInbox } from "./contract-session-update-inbox"; - -const TERMINAL_OUTPUT_EXTENSION = "agentclientprotocol.v1/terminal-output"; -const { cause: providerCause, provenance } = createProviderMeta("agent-client-protocol"); - -interface PendingTerminalExit { - readonly endedAt: string; - readonly exit: WaitForTerminalExitResponse; -} - -interface TerminalIntent { - readonly receivedAt: string; - readonly request: CreateTerminalRequest | undefined; - readonly runId: string; - readonly terminalId: string; -} - -interface UnknownTerminal { - readonly error: AuthorityOutcomeUnknownError; - readonly intent: TerminalIntent; - retry?: Promise; -} - -export interface AcpContractTerminalProjectorOptions { - readonly assertNativeSession: (sessionId: string) => void; - readonly inbox: AcpContractSessionUpdateInbox; - readonly now: () => string; - readonly projection: ContractProjection; - readonly resolveId: (runId: string, kind: string, nativeId: string) => string; - readonly withReceiptTime: (receivedAt: string, operation: () => Promise) => Promise; -} - -export class AcpContractTerminalProjector { - readonly #assertNativeSession: (sessionId: string) => void; - #disposed = false; - readonly #inbox: AcpContractSessionUpdateInbox; - readonly #now: () => string; - readonly #pendingTerminalExits = new Map(); - readonly #projection: ContractProjection; - readonly #resolveId: AcpContractTerminalProjectorOptions["resolveId"]; - readonly #truncatedTerminals = new Set(); - #unknownTerminal: UnknownTerminal | undefined; - readonly #withReceiptTime: AcpContractTerminalProjectorOptions["withReceiptTime"]; - - constructor(options: AcpContractTerminalProjectorOptions) { - this.#assertNativeSession = options.assertNativeSession; - this.#inbox = options.inbox; - this.#now = options.now; - this.#projection = options.projection; - this.#resolveId = options.resolveId; - this.#withReceiptTime = options.withReceiptTime; - } - - async registerTerminal( - runId: string, - terminalId: string, - request?: CreateTerminalRequest, - ): Promise { - this.#assertActive(); - - const snapshot = request === undefined ? undefined : structuredClone(request); - - if (snapshot !== undefined) { - this.#assertNativeSession(snapshot.sessionId); - } - - const unknownAtAdmission = this.#unknownTerminal; - const exactRetry = - unknownAtAdmission !== undefined && - unknownAtAdmission.intent.runId === runId && - unknownAtAdmission.intent.terminalId === terminalId && - isDeepStrictEqual(unknownAtAdmission.intent.request, snapshot) - ? unknownAtAdmission - : undefined; - - if (unknownAtAdmission !== undefined && exactRetry === undefined) { - throw unknownAtAdmission.error; - } - - if (exactRetry?.retry !== undefined) { - return exactRetry.retry; - } - - const intent = - exactRetry?.intent ?? - ({ - receivedAt: this.#projection.now().toISOString(), - request: snapshot, - runId, - terminalId, - } satisfies TerminalIntent); - const registration = this.#inbox.enqueue(async () => { - try { - const unknown = this.#unknownTerminal; - - if (unknown !== undefined && unknown !== unknownAtAdmission) { - throw unknown.error; - } - - const id = await this.#withReceiptTime(intent.receivedAt, () => - this.ensureTerminal(intent.runId, intent.terminalId, intent.request), - ); - - if (this.#unknownTerminal === unknownAtAdmission) { - this.#unknownTerminal = undefined; - } - - return id; - } catch (error) { - if (error instanceof AuthorityOutcomeUnknownError) { - this.#unknownTerminal ??= { error, intent }; - } else if (this.#unknownTerminal?.intent === intent) { - this.#unknownTerminal = undefined; - } - - throw error; - } - }); - - if (exactRetry !== undefined) { - const retry = registration.finally(() => { - if (exactRetry.retry === retry) { - delete exactRetry.retry; - } - }); - exactRetry.retry = retry; - return retry; - } - - return registration; - } - - async handleTerminalOutput( - runId: string, - terminalId: string, - response: TerminalOutputResponse, - ): Promise { - this.#assertActive(); - return this.#inbox.enqueue(() => this.#handleTerminalOutput(runId, terminalId, response)); - } - - async #handleTerminalOutput( - runId: string, - terminalId: string, - response: TerminalOutputResponse, - ): Promise { - if (this.#projection.run(runId)?.status !== "active") { - return; - } - - const id = await this.ensureTerminal(runId, terminalId); - const item = this.#projection.item(runId, id); - - if (item?.kind !== "terminal" || item.status !== "active") { - return; - } - - const truncationKey = `${runId}\0${terminalId}`; - - if (response.truncated) { - this.#truncatedTerminals.add(truncationKey); - } - - const pendingExit = this.#pendingTerminalExits.get(truncationKey); - const exit = response.exitStatus ?? pendingExit?.exit; - - if (exit === undefined || exit === null) { - await this.#projection.replacePreview({ - channel: "terminal.stdout", - itemId: id, - runId, - text: response.output, - }); - return; - } - - if (pendingExit !== undefined && !isDeepStrictEqual(pendingExit.exit, exit)) { - throw new Error(`ACP v1 terminal ${terminalId} changed its exit status.`); - } - - const terminalExit = pendingExit ?? { endedAt: this.#now(), exit }; - this.#pendingTerminalExits.set(truncationKey, terminalExit); - - await this.#finishTerminal( - runId, - item, - response.output, - terminalExit, - this.#truncatedTerminals.has(truncationKey), - ); - this.#pendingTerminalExits.delete(truncationKey); - this.#truncatedTerminals.delete(truncationKey); - } - - async handleTerminalExit( - runId: string, - terminalId: string, - response: WaitForTerminalExitResponse, - ): Promise { - this.#assertActive(); - return this.#inbox.enqueue(() => this.#handleTerminalExit(runId, terminalId, response)); - } - - async #handleTerminalExit( - runId: string, - terminalId: string, - response: WaitForTerminalExitResponse, - ): Promise { - if (this.#projection.run(runId)?.status !== "active") { - return; - } - - const id = await this.ensureTerminal(runId, terminalId); - const truncationKey = `${runId}\0${terminalId}`; - const item = this.#projection.item(runId, id); - - if (item?.kind === "terminal" && item.status === "active") { - const pending = this.#pendingTerminalExits.get(truncationKey); - - if (pending !== undefined && !isDeepStrictEqual(pending.exit, response)) { - throw new Error(`ACP v1 terminal ${terminalId} changed its exit status.`); - } - - this.#pendingTerminalExits.set(truncationKey, { - endedAt: pending?.endedAt ?? this.#now(), - exit: response, - }); - } - } - - async ensureTerminal( - runId: string, - terminalId: string, - request?: CreateTerminalRequest, - ): Promise { - const id = this.#resolveId(runId, "terminal", terminalId); - const existing = this.#projection.item(runId, id); - - if (existing !== undefined) { - if (existing.kind !== "terminal") { - throw new Error("ACP v1 terminal ID collided with a non-terminal item."); - } - - return id; - } - - const now = this.#now(); - await this.#projection.putItem( - runId, - "terminal/created", - providerCause("terminal/created", terminalId), - itemSchema.parse({ - audience: "participants", - ...(request === undefined - ? {} - : { - command: [request.command, ...(request.args ?? [])].join(" "), - ...(request.cwd === undefined || request.cwd === null ? {} : { cwd: request.cwd }), - }), - createdAt: now, - id, - kind: "terminal", - provenance: provenance("terminal/created", { terminalId }), - runId, - status: "active", - stderr: [], - stdout: [], - updatedAt: now, - }), - ); - return id; - } - - async #finishTerminal( - runId: string, - item: Extract, - output: string, - terminalExit: PendingTerminalExit, - truncated: boolean, - ): Promise { - const { endedAt: now, exit } = terminalExit; - const failed = - (exit.exitCode !== undefined && exit.exitCode !== null && exit.exitCode !== 0) || - (exit.signal !== undefined && exit.signal !== null); - await this.#projection.putItem( - runId, - "terminal/exited", - providerCause("terminal/exited", item.id), - itemSchema.parse({ - ...item, - endedAt: now, - ...(failed - ? { - error: { - code: "agent_client_protocol.terminal_failed", - message: "Terminal command failed.", - retryable: false, - }, - } - : {}), - ...(truncated - ? { extensions: { ...item.extensions, [TERMINAL_OUTPUT_EXTENSION]: { truncated } } } - : {}), - exitCode: exit.exitCode ?? null, - signal: exit.signal ?? null, - status: failed ? "failed" : "completed", - stdout: output.length === 0 ? [] : [{ text: output, type: "text" }], - updatedAt: now, - }), - ); - } - - async flushTerminalExits(runId: string): Promise { - for (const [key, pending] of this.#pendingTerminalExits) { - if (!key.startsWith(`${runId}\0`)) { - continue; - } - - const terminalId = key.slice(runId.length + 1); - const id = this.#resolveId(runId, "terminal", terminalId); - const item = this.#projection.item(runId, id); - - if (item?.kind === "terminal" && item.status === "active") { - await this.#finishTerminal( - runId, - item, - this.#projection.materializedText(runId, id, "terminal.stdout"), - pending, - this.#truncatedTerminals.has(key), - ); - } - - this.#pendingTerminalExits.delete(key); - this.#truncatedTerminals.delete(key); - } - } - - releaseRun(runId: string): void { - for (const key of this.#truncatedTerminals) { - if (key.startsWith(`${runId}\0`)) { - this.#truncatedTerminals.delete(key); - } - } - for (const key of this.#pendingTerminalExits.keys()) { - if (key.startsWith(`${runId}\0`)) { - this.#pendingTerminalExits.delete(key); - } - } - } - - dispose(): void { - this.#disposed = true; - this.#pendingTerminalExits.clear(); - this.#truncatedTerminals.clear(); - this.#unknownTerminal = undefined; - } - - #assertActive(): void { - if (this.#disposed) { - throw new Error("ACP v1 terminal projector is disposed."); - } - } -} diff --git a/src/runtimes/acp/v1-contract-adapter.ts b/src/runtimes/acp/v1-contract-adapter.ts deleted file mode 100644 index 6d280fa..0000000 --- a/src/runtimes/acp/v1-contract-adapter.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import type { - CreateTerminalRequest, - PromptResponse, - RequestPermissionRequest, - RequestPermissionResponse, - SessionNotification, - SessionUpdate, - TerminalOutputResponse, - WaitForTerminalExitResponse, -} from "@agentclientprotocol/sdk"; - -import type { InteractionResolution, Run } from "../../contract"; -import { createDriverId } from "../../protocol/id"; -import { - createProviderMeta, - ContractProjection, - type ContractProjectionOptions, -} from "../contract-projection"; -import { AcpContractSessionUpdateInbox } from "./contract-session-update-inbox"; -import { AcpContractItemProjector } from "./contract-item-projector"; -import { AcpContractPermissionController } from "./contract-permission-controller"; -import { AcpContractTerminalProjector } from "./contract-terminal-projector"; -import { toUsage } from "./contract-mapping"; - -export { toConfigOptions } from "./contract-mapping"; - -const DEFAULT_INTERACTION_TIMEOUT_MS = 5 * 60 * 1_000; -const DEFAULT_MAX_PENDING_PERMISSION_BYTES = 8 * 1_024 * 1_024; -const PROVIDER = "agent-client-protocol"; -const { cause: providerCause } = createProviderMeta(PROVIDER); - -export interface AcpV1ContractAdapterOptions extends ContractProjectionOptions { - readonly createId?: (() => string) | undefined; - readonly interactionTimeoutMs?: number | undefined; - readonly maxPendingPermissionBytes?: number | undefined; - readonly nativeSessionId: string; -} - -export class AcpV1ContractAdapter { - readonly #createId: () => string; - #disposed = false; - readonly #ids = new Map>(); - readonly #inbox: AcpContractSessionUpdateInbox; - readonly #items: AcpContractItemProjector; - readonly #nativeSessionId: string; - readonly #permissions: AcpContractPermissionController; - readonly #projection: ContractProjection; - #receivedAt: string | null = null; - readonly #terminals: AcpContractTerminalProjector; - - constructor(options: AcpV1ContractAdapterOptions) { - this.#createId = options.createId ?? createDriverId; - const interactionTimeoutMs = options.interactionTimeoutMs ?? DEFAULT_INTERACTION_TIMEOUT_MS; - const maxPendingPermissionBytes = - options.maxPendingPermissionBytes ?? DEFAULT_MAX_PENDING_PERMISSION_BYTES; - this.#nativeSessionId = options.nativeSessionId; - this.#projection = new ContractProjection(options); - this.#inbox = new AcpContractSessionUpdateInbox({ - apply: (runId, notification, receivedAt) => - this.#withReceipt(receivedAt, () => this.#applySessionUpdate(runId, notification)), - now: () => this.#projection.now(), - }); - this.#terminals = new AcpContractTerminalProjector({ - assertNativeSession: (sessionId) => this.#assertSession(sessionId), - inbox: this.#inbox, - now: () => this.#timestamp(), - projection: this.#projection, - resolveId: (runId, kind, nativeId) => this.#id(runId, kind, nativeId), - withReceiptTime: (receivedAt, operation) => this.#withReceipt(receivedAt, operation), - }); - this.#items = new AcpContractItemProjector({ - now: () => this.#timestamp(), - projection: this.#projection, - resolveId: (runId, kind, nativeId) => this.#id(runId, kind, nativeId), - terminals: this.#terminals, - }); - this.#permissions = new AcpContractPermissionController({ - assertNativeSession: (sessionId) => this.#assertSession(sessionId), - createId: this.#createId, - inbox: this.#inbox, - interactionTimeoutMs, - items: this.#items, - maxPendingPermissionBytes, - now: () => this.#timestamp(), - projection: this.#projection, - resolveId: (runId, kind, nativeId) => this.#id(runId, kind, nativeId), - withReceiptTime: (receivedAt, operation) => this.#withReceipt(receivedAt, operation), - }); - - if (this.#nativeSessionId.trim().length === 0) { - throw new Error("ACP v1 adapter requires a native session ID."); - } - - if ( - [interactionTimeoutMs, maxPendingPermissionBytes].some( - (value) => !Number.isSafeInteger(value) || value < 1, - ) - ) { - throw new RangeError("ACP v1 adapter limits must be finite and positive."); - } - } - - attachRun(run: Run): void { - this.#assertActive(); - this.#projection.attachRun(run); - } - - async handleSessionUpdate( - runId: string, - notification: SessionNotification, - ): Promise { - this.#assertActive(); - return this.#inbox.handle(runId, notification); - } - - async #applySessionUpdate( - runId: string, - notification: SessionNotification, - ): Promise { - this.#assertSession(notification.sessionId); - - const update = notification.update; - - switch (update.sessionUpdate) { - case "available_commands_update": - case "config_option_update": - case "current_mode_update": - case "session_info_update": - case "usage_update": - return update; - } - - if (this.#projection.run(runId)?.status !== "active") { - return null; - } - - switch (update.sessionUpdate) { - case "agent_message_chunk": - await this.#items.putMessageChunk(runId, update, "message"); - return null; - case "agent_thought_chunk": - await this.#items.putMessageChunk(runId, update, "reasoning"); - return null; - case "user_message_chunk": - return null; - case "tool_call": - case "tool_call_update": - await this.#items.putTool(runId, update, `session/${update.sessionUpdate}`); - return null; - case "plan": - await this.#items.putPlan(runId, "current", update.entries, "session/plan"); - return null; - default: - return null; - } - } - - async completePrompt(runId: string, response: PromptResponse): Promise { - this.#assertActive(); - return this.#inbox.enqueue(() => this.#completePrompt(runId, response)); - } - - async #completePrompt(runId: string, response: PromptResponse): Promise { - this.#inbox.throwIfFailed(); - - if (this.#projection.run(runId)?.status !== "active") { - return; - } - - await this.#terminals.flushTerminalExits(runId); - - const event = `prompt/${response.stopReason}`; - const cause = providerCause(event, runId); - - if (response.usage !== undefined && response.usage !== null) { - const current = this.#projection.run(runId)?.usage; - const usage = toUsage(response.usage, current); - - if (Object.keys(usage).length > 0 && !isDeepStrictEqual(usage, current)) { - await this.#projection.updateUsage(runId, event, cause, usage); - } - } - - if (response.stopReason === "cancelled") { - await this.#projection.finishRun({ cause, event, runId, status: "cancelled" }); - } else { - await this.#projection.finishRun({ - cause, - event, - finishReason: - response.stopReason === "refusal" - ? "refusal" - : response.stopReason === "max_tokens" || response.stopReason === "max_turn_requests" - ? "limit" - : "success", - runId, - status: "completed", - }); - } - - this.#releaseRun(runId); - } - - openPermission(runId: string, request: RequestPermissionRequest): Promise { - this.#assertActive(); - return this.#permissions.openPermission(runId, request); - } - - resolveInteraction( - interactionId: string, - resolution: InteractionResolution, - ): Promise { - this.#assertActive(); - return this.#permissions.resolveInteraction(interactionId, resolution); - } - - registerTerminal( - runId: string, - terminalId: string, - request?: CreateTerminalRequest, - ): Promise { - this.#assertActive(); - return this.#terminals.registerTerminal(runId, terminalId, request); - } - - handleTerminalOutput( - runId: string, - terminalId: string, - response: TerminalOutputResponse, - ): Promise { - this.#assertActive(); - return this.#terminals.handleTerminalOutput(runId, terminalId, response); - } - - handleTerminalExit( - runId: string, - terminalId: string, - response: WaitForTerminalExitResponse, - ): Promise { - this.#assertActive(); - return this.#terminals.handleTerminalExit(runId, terminalId, response); - } - - dispose(): void { - this.#disposed = true; - this.#ids.clear(); - this.#permissions.dispose(); - this.#inbox.close(); - this.#terminals.dispose(); - this.#projection.dispose(); - } - - #id(runId: string, kind: string, nativeId: string): string { - const candidate = nativeId.length > 0 ? `${kind}:${nativeId}` : ""; - - if (candidate.length > 0 && candidate.length <= 256) { - return candidate; - } - - let ids = this.#ids.get(runId); - - if (ids === undefined) { - ids = new Map(); - this.#ids.set(runId, ids); - } - - const key = `${kind}:${nativeId}`; - let id = ids.get(key); - - if (id === undefined) { - id = this.#createId(); - ids.set(key, id); - } - - return id; - } - - #assertSession(sessionId: string): void { - if (sessionId !== this.#nativeSessionId) { - throw new Error("ACP v1 message does not belong to the active native session."); - } - } - - #releaseRun(runId: string): void { - this.#ids.delete(runId); - this.#permissions.releaseRun(runId); - this.#terminals.releaseRun(runId); - } - - #timestamp(): string { - return this.#receivedAt ?? this.#projection.now().toISOString(); - } - - async #withReceipt(receivedAt: string, operation: () => Promise): Promise { - const previous = this.#receivedAt; - this.#receivedAt = receivedAt; - - try { - return await operation(); - } finally { - this.#receivedAt = previous; - } - } - - #assertActive(): void { - if (this.#disposed) { - throw new Error("ACP v1 adapter is disposed."); - } - } -} diff --git a/src/runtimes/claude/agent-sdk-driver-backend.ts b/src/runtimes/claude/agent-sdk-driver-backend.ts index 365756b..f5f87be 100644 --- a/src/runtimes/claude/agent-sdk-driver-backend.ts +++ b/src/runtimes/claude/agent-sdk-driver-backend.ts @@ -24,6 +24,7 @@ import type { AgentDriverBackend, AgentDriverContext } from "../../core/agent-dr import { DriverEventPublisher } from "../driver-event-publisher"; import { computeRuntimeBootstrapDigest, writeSkillBootstrapArtifacts } from "../skill-bootstrap"; import { readProcessEnvString, toErrorMessage } from "./agent-sdk-json"; +import { ClaudeDurableEventTooLargeError } from "./agent-sdk-event-writer"; import { ClaudeAgentSdkPrewarm } from "./agent-sdk-prewarm"; import { ClaudeAgentSdkMessageTranslator, @@ -35,7 +36,8 @@ import { resolveClaudeConfigDir, } from "./agent-sdk-query-options"; import { buildClaudeRecoveryPrompt } from "./agent-sdk-recovery-context"; -import { readClaudeNativeResumeSessionId } from "./agent-sdk-resume"; +import { readClaudeNativeResumeSessionId, requireClaudeNativeSessionId } from "./agent-sdk-resume"; +import { ClaudePublicToolCallIdState } from "./agent-sdk-tool-id"; import { drainClaudeTasks } from "./agent-sdk-tasks"; interface ActiveClaudeTurn { @@ -76,6 +78,7 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { readonly #payload: DriverStartInput; readonly #pendingProcessTasks = new Set>(); readonly #prewarm: ClaudeAgentSdkPrewarm; + readonly #publicToolCallIds = new ClaudePublicToolCallIdState(); #activeTurn: ActiveClaudeTurn | null = null; #nativeSessionId: string | null = null; #stopRequested = false; @@ -92,12 +95,18 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { createQueryOptions: this.#dependencies.createQueryOptions, getNativeSessionId: () => this.#nativeSessionId, payload, + publicToolCallId: (nativeToolCallId) => this.#publicToolCallIds.publicId(nativeToolCallId), startup: async (input) => this.#dependencies.startup(input), }); this.#messageTranslator = new ClaudeAgentSdkMessageTranslator({ + publicToolCallId: (nativeToolCallId) => this.#publicToolCallIds.publicId(nativeToolCallId), push: async (context, reason, events) => this.#push(context, reason, events), + pushTerminal: async (context, reason, closures, terminal) => + this.#eventPublisher.pushTerminal(context, reason, closures, terminal), recordNativeSessionId: async (context, sessionId) => this.#recordNativeSessionId(context, sessionId), + replaceNativeSessionId: async (context, previousSessionId, nextSessionId) => + this.#replaceNativeSessionId(context, previousSessionId, nextSessionId), }); } @@ -160,6 +169,7 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { } this.#messageTranslator.resetTurnMessageState(); + this.#publicToolCallIds.reset(); // With no native session to resume, every query starts a fresh provider // session, so the bounded platform-history replay must ride the prompt of @@ -215,6 +225,8 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { payload: this.#payload, permissionTasks, processTasks, + publicToolCallId: (nativeToolCallId) => + this.#publicToolCallIds.publicId(nativeToolCallId), }); queryOptionsMs = Date.now() - optionsStartedAtMs; @@ -334,40 +346,28 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { if (isTurnCancelled(activeTurn)) { await this.#closeQuery(context, activeTurn, activeTurn.cancelReason ?? "turn.cancelled"); - await this.#messageTranslator.finishTurn(context, "failed").catch(() => {}); - await this.#push(context, "driver.claude.turn.cancelled", [ - { - kind: "run.cancelled", - payload: { - reason: activeTurn.cancelReason ?? "turn.cancelled", - requestedBy: "user", - stopReason: "cancelled", - }, - runId, - }, - ]); + await this.#messageTranslator.cancelTurn( + context, + runId, + activeTurn.cancelReason ?? "turn.cancelled", + ); throw new DriverTurnCancelledError("Claude Agent SDK turn was cancelled."); } activeTurn.state = "finalizing"; await this.#closeQuery(context, activeTurn, "turn.failed"); - await this.#messageTranslator.finishTurn(context, "failed").catch(() => {}); if (error instanceof ClaudeTerminalWriteError) { throw error.cause; } const message = toErrorMessage(error, "Claude Agent SDK turn failed."); - await this.#push(context, "driver.claude.turn.failed", [ - { - kind: "run.failed", - payload: { - error: { code: "claude.turn_failed", message }, - recoverable: false, - }, - runId, - }, - ]); + await this.#messageTranslator.failTurn( + context, + runId, + error instanceof ClaudeDurableEventTooLargeError ? error.code : "claude.turn_failed", + message, + ); throw error; } finally { try { @@ -488,9 +488,7 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { } async #recordNativeSessionId(context: AgentDriverContext, sessionId: string): Promise { - if (sessionId.trim().length === 0) { - throw new Error("Claude Agent SDK message has an empty native session ID."); - } + requireClaudeNativeSessionId(sessionId); if (this.#nativeSessionId === sessionId) { return; @@ -500,8 +498,44 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { throw new Error("Claude Agent SDK message belongs to a different native session."); } + const previousSessionId = this.#nativeSessionId; this.#nativeSessionId = sessionId; - await this.#publishNativeResumeRef(context); + try { + await this.#publishNativeResumeRef(context, sessionId); + } catch (error) { + if (this.#nativeSessionId === sessionId) { + this.#nativeSessionId = previousSessionId; + } + throw error; + } + } + + async #replaceNativeSessionId( + context: AgentDriverContext, + previousSessionId: string, + nextSessionId: string, + ): Promise { + requireClaudeNativeSessionId(previousSessionId); + requireClaudeNativeSessionId(nextSessionId); + + if (this.#nativeSessionId === nextSessionId) { + return; + } + + if (this.#nativeSessionId !== null && this.#nativeSessionId !== previousSessionId) { + throw new Error("Claude conversation reset belongs to a different native session."); + } + + const retainedSessionId = this.#nativeSessionId; + this.#nativeSessionId = nextSessionId; + try { + await this.#publishNativeResumeRef(context, nextSessionId); + } catch (error) { + if (this.#nativeSessionId === nextSessionId) { + this.#nativeSessionId = retainedSessionId; + } + throw error; + } } async #closeQuery( @@ -529,16 +563,15 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { await turn.queryCloseTask; } - async #publishNativeResumeRef(context: AgentDriverContext): Promise { - if (!this.#nativeSessionId) { - throw new Error("Claude native session id is required before publishing resume ref."); - } - + async #publishNativeResumeRef( + context: AgentDriverContext, + nativeSessionId: string, + ): Promise { await this.#push(context, "driver.claude.native_resume_ref.updated", [ { kind: "runtime.resume.updated", payload: { - resumePointer: this.#nativeSessionId, + resumePointer: nativeSessionId, threadId: null, }, visibility: "owner_debug", diff --git a/src/runtimes/claude/agent-sdk-event-writer.ts b/src/runtimes/claude/agent-sdk-event-writer.ts index c545a21..dfdabd4 100644 --- a/src/runtimes/claude/agent-sdk-event-writer.ts +++ b/src/runtimes/claude/agent-sdk-event-writer.ts @@ -2,17 +2,48 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; import type { DriverEventInput } from "../../protocol/events"; import type { MessageId, RunId } from "../../protocol/id"; +import type { JsonValue } from "../../protocol/json"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; +import { chunkJsonText } from "../provider-json"; import type { JsonObject } from "./agent-sdk-json"; import { toClaudeDiagnosticEvent, toClaudeUsageUpdatedEvents } from "./agent-sdk-message-events"; +const MAX_CLAUDE_MESSAGE_CHUNK_BYTES = 512 * 1_024; +const MAX_CLAUDE_DURABLE_EVENT_BYTES = 1_020 * 1_024; + +function claudeDurableEventBytes(event: DriverEventInput): number { + return Buffer.byteLength(JSON.stringify(event), "utf8"); +} + +export class ClaudeDurableEventTooLargeError extends RangeError { + readonly code: string; + + constructor(code: string, subject: string, bytes: number) { + super(`Claude ${subject} exceeds durable event capacity (${String(bytes)} UTF-8 bytes).`); + this.code = code; + this.name = "ClaudeDurableEventTooLargeError"; + } +} + +export function assertClaudeDurableEventFits( + event: DriverEventInput, + code: string, + subject: string, +): void { + const bytes = claudeDurableEventBytes(event); + + if (bytes > MAX_CLAUDE_DURABLE_EVENT_BYTES) { + throw new ClaudeDurableEventTooLargeError(code, subject, bytes); + } +} + interface ClaudeAgentSdkEventWriterOptions { push(context: AgentDriverContext, reason: string, events: DriverEventInput[]): Promise; } export interface ClaudeToolStartEvent { context: AgentDriverContext; - parentMessageId: string; + parentMessageId?: string; toolCallId: string; toolCallName: string; } @@ -38,15 +69,30 @@ export interface ClaudeToolArgumentsEvent { } export interface ClaudeToolResultEvent { + agentId?: string; + authoritative?: boolean; content: string; context: AgentDriverContext; - messageId: string; - status: "completed" | "failed"; + decisionReason?: string; + decisionReasonType?: string; + messageId?: string; + nonExecutionKind?: string; + rawInput?: string; + status: "cancelled" | "completed" | "failed"; + structuredOutput?: JsonValue; toolCallId: string; + toolCallName?: string; + userFeedback?: string; +} + +export interface ClaudeTurnClosure { + readonly commit: () => void; + readonly events: readonly DriverEventInput[]; } export class ClaudeAgentSdkEventWriter { readonly #messageEnded = new Set(); + readonly #messageRetracted = new Set(); readonly #messageSealed = new Set(); readonly #messageStarted = new Set(); readonly #options: ClaudeAgentSdkEventWriterOptions; @@ -55,6 +101,7 @@ export class ClaudeAgentSdkEventWriter { readonly #toolEnded = new Set(); readonly #toolParentMessage = new Map(); readonly #toolStarted = new Set(); + readonly #toolRetracted = new Set(); constructor(options: ClaudeAgentSdkEventWriterOptions) { this.#options = options; @@ -66,6 +113,7 @@ export class ClaudeAgentSdkEventWriter { resetTurnState(): void { this.#messageEnded.clear(); + this.#messageRetracted.clear(); this.#messageSealed.clear(); this.#messageStarted.clear(); this.#thoughtEnded.clear(); @@ -73,6 +121,7 @@ export class ClaudeAgentSdkEventWriter { this.#toolEnded.clear(); this.#toolParentMessage.clear(); this.#toolStarted.clear(); + this.#toolRetracted.clear(); } toolParentMessageId(toolCallId: string): string | null { @@ -84,8 +133,7 @@ export class ClaudeAgentSdkEventWriter { return false; } - this.#messageEnded.add(messageId); - await this.#push(context, "driver.claude.message.ended", [ + await this.#options.push(context, "driver.claude.message.ended", [ { kind: "message.completed", payload: { @@ -94,6 +142,80 @@ export class ClaudeAgentSdkEventWriter { }, }, ]); + this.#messageEnded.add(messageId); + return true; + } + + async cancelMessage(context: AgentDriverContext, messageId: string): Promise { + if (!this.#messageStarted.has(messageId) || this.#messageEnded.has(messageId)) { + return false; + } + + await this.#options.push(context, "driver.claude.message.cancelled", [ + { + kind: "message.cancelled", + payload: { messageId, role: "agent" }, + }, + ]); + this.#messageEnded.add(messageId); + return true; + } + + async retractMessage(context: AgentDriverContext, messageId: string): Promise { + if (!this.#messageStarted.has(messageId) || this.#messageRetracted.has(messageId)) { + return; + } + + await this.#options.push(context, "driver.claude.message.retracted", [ + { + kind: "message.cancelled", + payload: { messageId, reason: "superseded", role: "agent" }, + }, + ]); + this.#messageRetracted.add(messageId); + this.#messageEnded.add(messageId); + } + + async retractTool(context: AgentDriverContext, toolCallId: string): Promise { + if (!this.#toolStarted.has(toolCallId) || this.#toolRetracted.has(toolCallId)) { + return; + } + + const ended = this.#toolEnded.has(toolCallId); + await this.#options.push(context, "driver.claude.tool.retracted", [ + { + kind: "tool.call.updated", + payload: { status: "cancelled", toolCallId }, + }, + ...(ended + ? [] + : [ + { + kind: "item.completed" as const, + payload: { itemId: toolCallId, itemType: "tool_call", status: "cancelled" }, + }, + ]), + ]); + this.#toolRetracted.add(toolCallId); + this.#toolEnded.add(toolCallId); + } + + async failMessage( + context: AgentDriverContext, + messageId: string, + error: { readonly code: string; readonly message: string; readonly retryable: boolean }, + ): Promise { + if (!this.#messageStarted.has(messageId) || this.#messageEnded.has(messageId)) { + return false; + } + + await this.#options.push(context, "driver.claude.message.failed", [ + { + kind: "message.failed", + payload: { error, messageId, role: "agent" }, + }, + ]); + this.#messageEnded.add(messageId); return true; } @@ -102,8 +224,7 @@ export class ClaudeAgentSdkEventWriter { return; } - this.#messageStarted.add(messageId); - await this.#push(context, "driver.claude.message.started", [ + await this.#options.push(context, "driver.claude.message.started", [ { kind: "message.started", payload: { @@ -112,6 +233,7 @@ export class ClaudeAgentSdkEventWriter { }, }, ]); + this.#messageStarted.add(messageId); } async ensureToolStarted({ @@ -124,16 +246,13 @@ export class ClaudeAgentSdkEventWriter { return; } - await this.ensureMessageStarted(context, parentMessageId); - this.#toolStarted.add(toolCallId); - this.#toolParentMessage.set(toolCallId, parentMessageId); - await this.#push(context, "driver.claude.tool.started", [ + const events: DriverEventInput[] = [ { kind: "item.started", payload: { itemId: toolCallId, itemType: "tool_call", - parentMessageId, + ...(parentMessageId === undefined ? {} : { parentMessageId }), title: toolCallName, }, }, @@ -141,96 +260,183 @@ export class ClaudeAgentSdkEventWriter { kind: "tool.call.updated", payload: { kind: "tool", - parentMessageId, + ...(parentMessageId === undefined ? {} : { parentMessageId }), status: "running", title: toolCallName, toolCallId, }, }, - ]); + ]; + + for (const event of events) { + assertClaudeDurableEventFits(event, "claude.tool_start_too_large", "tool start"); + } + + if (parentMessageId !== undefined) { + await this.ensureMessageStarted(context, parentMessageId); + } + await this.#options.push(context, "driver.claude.tool.started", events); + if (parentMessageId !== undefined) { + this.#toolParentMessage.set(toolCallId, parentMessageId); + } + this.#toolStarted.add(toolCallId); } async pushDiagnostic(context: AgentDriverContext, message: SDKMessage): Promise { - await this.pushRaw(context, "driver.claude.diagnostic", toClaudeDiagnosticEvent(message)); + await this.pushRawDiagnostic( + context, + "driver.claude.diagnostic", + toClaudeDiagnosticEvent(message), + ); } - async pushRaw(context: AgentDriverContext, reason: string, event: JsonObject): Promise { - await this.#push(context, reason, [ + async pushRawDiagnostic( + context: AgentDriverContext, + reason: string, + event: JsonObject, + options: { + readonly message?: string; + readonly severity?: "error" | "info" | "warn"; + } = {}, + ): Promise { + await this.#options.push(context, reason, [ { + delivery: "best_effort", kind: "diagnostic.reported", payload: { - message: reason, + message: options.message ?? reason, raw: event, - severity: "info", + severity: options.severity ?? "info", }, visibility: "owner_debug", }, ]); } - async pushRunError( - context: AgentDriverContext, + runError( runId: RunId, code: string, message: string, - ): Promise { - await this.#push(context, "driver.claude.turn.failed", [ - { - kind: "run.failed", - payload: { - error: { - code, - message, + retryable: boolean, + details?: JsonObject, + ): DriverEventInput { + const event: DriverEventInput = { + kind: "run.failed", + payload: { + error: { + code, + ...(details === undefined ? {} : { details }), + message, + retryable, + }, + recoverable: retryable, + }, + runId, + }; + + if (claudeDurableEventBytes(event) <= MAX_CLAUDE_DURABLE_EVENT_BYTES) { + return event; + } + + return { + kind: "run.failed", + payload: { + error: { + code, + details: { + ...(details === undefined + ? {} + : { originalDetailsUtf8Bytes: Buffer.byteLength(JSON.stringify(details), "utf8") }), + originalMessageUtf8Bytes: Buffer.byteLength(message, "utf8"), }, - recoverable: false, + message: "Claude Agent SDK failure exceeded durable event capacity.", + retryable, }, - runId, + recoverable: retryable, }, - ]); + runId, + }; } - async pushRunFinished( - context: AgentDriverContext, + runFinished( runId: RunId, - finalMessage: { id: MessageId; text: string } | null, - ): Promise { - await this.#push(context, "driver.claude.turn.completed", [ - { - runId, - kind: "run.completed", - payload: { - ...(finalMessage === null - ? {} - : { - finalMessageId: finalMessage.id, - finalMessageText: finalMessage.text, - }), - stopReason: "end_turn", - }, + finalMessage: { readonly id: MessageId } | null, + structuredOutput?: JsonValue, + ): DriverEventInput { + return { + runId, + kind: "run.completed", + payload: { + ...(finalMessage === null + ? {} + : { + finalMessageId: finalMessage.id, + }), + stopReason: "end_turn", + ...(structuredOutput === undefined ? {} : { structuredOutput }), }, - ]); + }; + } + + runCancelled(runId: RunId, reason: string): DriverEventInput { + const event: DriverEventInput = { + kind: "run.cancelled", + payload: { reason, stopReason: "cancelled" }, + runId, + }; + + if (claudeDurableEventBytes(event) <= MAX_CLAUDE_DURABLE_EVENT_BYTES) { + return event; + } + + return { + kind: "run.cancelled", + payload: { + originalReasonUtf8Bytes: Buffer.byteLength(reason, "utf8"), + reason: "Claude cancellation reason exceeded durable event capacity.", + stopReason: "cancelled", + }, + runId, + }; } async pushMessageSnapshot( context: AgentDriverContext, messageId: string, text: string, + metadata: JsonObject = {}, ): Promise { if (this.#messageEnded.has(messageId)) { return false; } await this.ensureMessageStarted(context, messageId); - await this.#push(context, "driver.claude.message.snapshot", [ + const chunks = chunkJsonText(text, MAX_CLAUDE_MESSAGE_CHUNK_BYTES); + const events: DriverEventInput[] = [ { kind: "message.added", payload: { - content: [{ text, type: "text" }], + ...metadata, + content: [{ text: chunks[0]!, type: "text" }], messageId, role: "agent", }, }, - ]); + ...chunks.slice(1).map((contentDelta): DriverEventInput => ({ + kind: "message.delta", + payload: { contentDelta, messageId, role: "agent" }, + })), + ]; + + for (const event of events) { + assertClaudeDurableEventFits( + event, + "claude.message_snapshot_too_large", + `message snapshot ${messageId}`, + ); + } + + await this.#options.push(context, "driver.claude.message.snapshot", events); return true; } @@ -238,11 +444,12 @@ export class ClaudeAgentSdkEventWriter { this.#messageSealed.add(messageId); } - async pushSessionInfoUpdated(context: AgentDriverContext): Promise { - await this.#push(context, "driver.claude.session.info", [ + async pushSessionInfoUpdated(context: AgentDriverContext, resetTitle = false): Promise { + await this.#options.push(context, "driver.claude.session.info", [ { kind: "session.info.updated", payload: { + ...(resetTitle ? { title: null } : {}), updatedAt: new Date().toISOString(), }, }, @@ -260,7 +467,7 @@ export class ClaudeAgentSdkEventWriter { } await this.ensureMessageStarted(context, messageId); - await this.#push(context, reason, [ + await this.#options.push(context, reason, [ { delivery: "best_effort", kind: "message.delta", @@ -279,8 +486,7 @@ export class ClaudeAgentSdkEventWriter { return; } - this.#thoughtStarted.add(thoughtId); - await this.#push(context, "driver.claude.thought.started", [ + await this.#options.push(context, "driver.claude.thought.started", [ { kind: "thought.started", payload: { @@ -289,6 +495,7 @@ export class ClaudeAgentSdkEventWriter { }, }, ]); + this.#thoughtStarted.add(thoughtId); } async pushThoughtDelta({ context, delta, thoughtId }: ClaudeThoughtDeltaEvent): Promise { @@ -297,7 +504,7 @@ export class ClaudeAgentSdkEventWriter { } await this.ensureThoughtStarted(context, thoughtId); - await this.#push(context, "driver.claude.thought.delta", [ + await this.#options.push(context, "driver.claude.thought.delta", [ { delivery: "best_effort", kind: "thought.delta", @@ -315,8 +522,7 @@ export class ClaudeAgentSdkEventWriter { return; } - this.#thoughtEnded.add(thoughtId); - await this.#push(context, "driver.claude.thought.completed", [ + await this.#options.push(context, "driver.claude.thought.completed", [ { kind: "thought.completed", payload: { @@ -325,6 +531,21 @@ export class ClaudeAgentSdkEventWriter { }, }, ]); + this.#thoughtEnded.add(thoughtId); + } + + async cancelThought(context: AgentDriverContext, thoughtId: string): Promise { + if (!this.#thoughtStarted.has(thoughtId) || this.#thoughtEnded.has(thoughtId)) { + return; + } + + await this.#options.push(context, "driver.claude.thought.cancelled", [ + { + kind: "thought.cancelled", + payload: { channel: "summary", thoughtId }, + }, + ]); + this.#thoughtEnded.add(thoughtId); } async pushToolArguments({ @@ -337,7 +558,7 @@ export class ClaudeAgentSdkEventWriter { return; } - await this.#push(context, reason, [ + await this.#options.push(context, reason, [ { delivery: "best_effort", kind: "tool.call.updated", @@ -359,30 +580,29 @@ export class ClaudeAgentSdkEventWriter { return; } - await this.#push(context, "driver.claude.tool.snapshot", [ - { - kind: "tool.call.updated", - payload: { - rawInput, - status: "running", - toolCallId, - }, + const event: DriverEventInput = { + kind: "tool.call.updated", + payload: { + rawInput, + status: "running", + toolCallId, }, - ]); + }; + assertClaudeDurableEventFits(event, "claude.tool_input_too_large", `tool input ${toolCallId}`); + await this.#options.push(context, "driver.claude.tool.snapshot", [event]); } - async finishTools(context: AgentDriverContext, status: "completed" | "failed"): Promise { + async finishTools( + context: AgentDriverContext, + status: "cancelled" | "completed" | "failed", + ): Promise { const toolCallIds = [...this.#toolStarted].filter((id) => !this.#toolEnded.has(id)); if (toolCallIds.length === 0) { return; } - for (const toolCallId of toolCallIds) { - this.#toolEnded.add(toolCallId); - } - - await this.#push( + await this.#options.push( context, "driver.claude.tools.finished", toolCallIds.flatMap((toolCallId): DriverEventInput[] => [ @@ -396,16 +616,85 @@ export class ClaudeAgentSdkEventWriter { }, ]), ); + for (const toolCallId of toolCallIds) { + this.#toolEnded.add(toolCallId); + } + } + + prepareTurnClosure(status: "cancelled" | "completed" | "failed"): ClaudeTurnClosure { + const messageIds = [...this.#messageStarted].filter((id) => !this.#messageEnded.has(id)); + const thoughtIds = [...this.#thoughtStarted].filter((id) => !this.#thoughtEnded.has(id)); + const toolCallIds = [...this.#toolStarted].filter((id) => !this.#toolEnded.has(id)); + const events: DriverEventInput[] = [ + ...thoughtIds.map((thoughtId): DriverEventInput => ({ + kind: status === "completed" ? "thought.completed" : "thought.cancelled", + payload: { channel: "summary", thoughtId }, + })), + ...messageIds.map((messageId): DriverEventInput => ({ + kind: + status === "cancelled" + ? "message.cancelled" + : status === "failed" + ? "message.failed" + : "message.completed", + payload: + status === "failed" + ? { + error: { + code: "claude.turn_failed", + message: "Claude Agent SDK turn failed.", + retryable: false, + }, + messageId, + role: "agent", + } + : { messageId, role: "agent" }, + })), + ...toolCallIds.flatMap((toolCallId): DriverEventInput[] => [ + { + kind: "tool.call.updated", + payload: { status, toolCallId }, + }, + { + kind: "item.completed", + payload: { itemId: toolCallId, itemType: "tool_call", status }, + }, + ]), + ]; + + return { + commit: () => { + for (const thoughtId of thoughtIds) this.#thoughtEnded.add(thoughtId); + for (const messageId of messageIds) this.#messageEnded.add(messageId); + for (const toolCallId of toolCallIds) this.#toolEnded.add(toolCallId); + }, + events, + }; } async pushToolResult({ + agentId, + authoritative = false, content, context, + decisionReason, + decisionReasonType, messageId, + nonExecutionKind, + rawInput, status, + structuredOutput, toolCallId, + toolCallName, + userFeedback, }: ClaudeToolResultEvent): Promise { - if (this.#toolEnded.has(toolCallId)) { + if (this.#toolRetracted.has(toolCallId)) { + return; + } + + const ended = this.#toolEnded.has(toolCallId); + + if (ended && !authoritative) { return; } @@ -413,26 +702,43 @@ export class ClaudeAgentSdkEventWriter { { kind: "tool.call.updated", payload: { + ...(agentId === undefined ? {} : { agentId }), content, - messageId, - rawOutput: content, + ...(decisionReason === undefined ? {} : { decisionReason }), + ...(decisionReasonType === undefined ? {} : { decisionReasonType }), + ...(messageId === undefined ? {} : { messageId }), + ...(nonExecutionKind === undefined ? {} : { nonExecutionKind }), + ...(rawInput === undefined ? {} : { rawInput }), status, + ...(structuredOutput === undefined ? {} : { structuredOutput }), + ...(toolCallName === undefined ? {} : { title: toolCallName }), toolCallId, + ...(userFeedback === undefined ? {} : { userFeedback }), }, }, ]; - this.#toolEnded.add(toolCallId); - events.push({ - kind: "item.completed", - payload: { - itemId: toolCallId, - itemType: "tool_call", - status, - }, - }); + assertClaudeDurableEventFits( + events[0]!, + "claude.tool_result_too_large", + `tool result ${toolCallId}`, + ); + + if (!ended && this.#toolStarted.has(toolCallId)) { + events.push({ + kind: "item.completed", + payload: { + itemId: toolCallId, + itemType: "tool_call", + status, + }, + }); + } - await this.#push(context, "driver.claude.tool.result", events); + await this.#options.push(context, "driver.claude.tool.result", events); + if (!ended && this.#toolStarted.has(toolCallId)) { + this.#toolEnded.add(toolCallId); + } } async pushUsage( @@ -446,14 +752,6 @@ export class ClaudeAgentSdkEventWriter { return; } - await this.#push(context, "driver.claude.usage.updated", events); - } - - async #push( - context: AgentDriverContext, - reason: string, - events: DriverEventInput[], - ): Promise { - await this.#options.push(context, reason, events); + await this.#options.push(context, "driver.claude.usage.updated", events); } } diff --git a/src/runtimes/claude/agent-sdk-json.ts b/src/runtimes/claude/agent-sdk-json.ts index af11f34..9cb726f 100644 --- a/src/runtimes/claude/agent-sdk-json.ts +++ b/src/runtimes/claude/agent-sdk-json.ts @@ -1,13 +1,12 @@ -import type { JsonObject } from "../provider-json"; - -export { isRecord, readRecord, readString, stringifyForDisplay } from "../provider-json"; +export { + isRecord, + readNumber, + readRecord, + readString, + stringifyForDisplay, +} from "../provider-json"; export type { JsonObject } from "../provider-json"; -export function readNumber(value: JsonObject | null, key: string): number | null { - const entry = value?.[key]; - return typeof entry === "number" && Number.isFinite(entry) ? entry : null; -} - export function toTokenCount(value: unknown): number | null { return typeof value === "number" && value >= 0 && Number.isSafeInteger(value) ? value : null; } diff --git a/src/runtimes/claude/agent-sdk-message-events.ts b/src/runtimes/claude/agent-sdk-message-events.ts index 6207d47..31be521 100644 --- a/src/runtimes/claude/agent-sdk-message-events.ts +++ b/src/runtimes/claude/agent-sdk-message-events.ts @@ -22,9 +22,11 @@ export function toClaudeFilesPersistedEvents(message: SDKFilesPersistedEvent): D if (message.failed.length > 0) { events.push({ + delivery: "best_effort", kind: "diagnostic.reported", payload: { - failed: message.failed, + failedCount: message.failed.length, + failedUtf8Bytes: Buffer.byteLength(JSON.stringify(message.failed), "utf8"), message: "Claude file persistence failed.", severity: "warn", }, @@ -46,6 +48,33 @@ export function toClaudeDiagnosticEvent(message: SDKMessage): JsonObject { }; } +function sumModelUsage(modelUsage: unknown, key: string): number | null { + if (!isRecord(modelUsage)) { + return null; + } + + const counts = Object.values(modelUsage).flatMap((value) => { + const count = isRecord(value) ? toTokenCount(value[key]) : null; + return count === null ? [] : [count]; + }); + return counts.length === 0 ? null : toTokenCount(counts.reduce((total, count) => total + count)); +} + +export function aggregateClaudeModelUsage(modelUsage: unknown): JsonObject | null { + const inputTokens = sumModelUsage(modelUsage, "inputTokens"); + const outputTokens = sumModelUsage(modelUsage, "outputTokens"); + const cacheReadTokens = sumModelUsage(modelUsage, "cacheReadInputTokens"); + const cacheCreationTokens = sumModelUsage(modelUsage, "cacheCreationInputTokens"); + const usage: JsonObject = { + ...(cacheCreationTokens === null ? {} : { cache_creation_input_tokens: cacheCreationTokens }), + ...(cacheReadTokens === null ? {} : { cache_read_input_tokens: cacheReadTokens }), + ...(inputTokens === null ? {} : { input_tokens: inputTokens }), + ...(outputTokens === null ? {} : { output_tokens: outputTokens }), + }; + + return Object.keys(usage).length === 0 ? null : usage; +} + export function toClaudeUsageUpdatedEvents( usage: JsonObject | null, costAmount: number | null, diff --git a/src/runtimes/claude/agent-sdk-message-state.ts b/src/runtimes/claude/agent-sdk-message-state.ts index a3002ab..ed6a109 100644 --- a/src/runtimes/claude/agent-sdk-message-state.ts +++ b/src/runtimes/claude/agent-sdk-message-state.ts @@ -38,23 +38,28 @@ export function readClaudeSdkSessionId(value: unknown): string | null { export class ClaudeAgentSdkMessageState { readonly #activeAssistantMessageIds = new Map(); readonly #activeThoughtIds = new Map(); + readonly #auxiliaryMessageIds = new RuntimeAssistantMessageIdIndex(); readonly #authoritativeAssistantMessageIds = new Set(); readonly #assistantMessageIds = new RuntimeAssistantMessageIdIndex(); readonly #assistantMessageOrdinals = new Map(); readonly #assistantMessageRunIds = new Map(); readonly #assistantMessageSequences = new Map(); readonly #blockToolCallIds = new Map>(); - readonly #assistantNativeAliases = new Map(); + readonly #assistantWireAliases = new Map(); readonly #lastCompletedAssistantMessages = new Map(); readonly #pendingAssistantStreamAnchors = new Map(); readonly #streamedTextMessages = new Set(); readonly #streamingNativeMessageIds = new Map(); + readonly #streamingWireUuids = new Map(); readonly #textByAssistantMessageId = new Map(); + readonly #wireAssistantMessageIds = new Map(); + readonly #wireToolCallIds = new Map(); reset(): void { this.#activeAssistantMessageIds.clear(); this.#activeThoughtIds.clear(); - this.#assistantNativeAliases.clear(); + this.#auxiliaryMessageIds.reset(); + this.#assistantWireAliases.clear(); this.#authoritativeAssistantMessageIds.clear(); this.#assistantMessageIds.reset(); this.#assistantMessageOrdinals.clear(); @@ -65,7 +70,10 @@ export class ClaudeAgentSdkMessageState { this.#pendingAssistantStreamAnchors.clear(); this.#streamedTextMessages.clear(); this.#streamingNativeMessageIds.clear(); + this.#streamingWireUuids.clear(); this.#textByAssistantMessageId.clear(); + this.#wireAssistantMessageIds.clear(); + this.#wireToolCallIds.clear(); } assistantMessageId(runId: RunId, nativeMessageId: string | null): MessageId { @@ -88,10 +96,47 @@ export class ClaudeAgentSdkMessageState { return messageId; } + auxiliaryMessageId(runId: RunId, nativeMessageId: string): MessageId { + return this.#auxiliaryMessageIds.getOrCreate(`${runId}:${nativeMessageId}`); + } + assistantMessages(): readonly (readonly [MessageId, RunId])[] { return [...this.#assistantMessageRunIds]; } + bindWireAssistantMessage(wireUuid: string, messageId: MessageId): void { + this.#wireAssistantMessageIds.set(wireUuid, messageId); + } + + bindWireToolCalls(wireUuid: string, toolCallIds: readonly string[]): void { + if (toolCallIds.length > 0) { + this.#wireToolCallIds.set(wireUuid, [...toolCallIds]); + } + } + + wireItems(wireUuid: string): { + readonly messageId: MessageId | null; + readonly toolCallIds: readonly string[]; + } { + return { + messageId: this.#wireAssistantMessageIds.get(wireUuid) ?? null, + toolCallIds: this.#wireToolCallIds.get(wireUuid) ?? [], + }; + } + + commitWireMessageRetraction(wireUuid: string, messageId: MessageId): void { + if (this.#wireAssistantMessageIds.get(wireUuid) !== messageId) { + return; + } + + this.#wireAssistantMessageIds.delete(wireUuid); + this.#retractAssistantMessage(messageId); + } + + commitWireToolRetractions(wireUuid: string): void { + this.#wireToolCallIds.delete(wireUuid); + } + activeAssistantMessageId(runId: RunId): MessageId | undefined { return this.#activeAssistantMessageIds.get(runId); } @@ -141,16 +186,20 @@ export class ClaudeAgentSdkMessageState { return thoughtId; } - takeThoughtId(messageId: string): string | undefined { - const thoughtId = this.#activeThoughtIds.get(messageId); + activeThoughts(): readonly (readonly [string, string])[] { + return [...this.#activeThoughtIds]; + } + + thoughtIdForMessage(messageId: string): string | undefined { + return this.#activeThoughtIds.get(messageId); + } + + deleteThoughtId(messageId: string): void { this.#activeThoughtIds.delete(messageId); - return thoughtId; } - takeAllThoughtIds(): readonly string[] { - const thoughtIds = [...this.#activeThoughtIds.values()]; + clearThoughtIds(): void { this.#activeThoughtIds.clear(); - return thoughtIds; } streamScopeKey(runId: RunId, message: SDKMessage): string { @@ -160,6 +209,7 @@ export class ClaudeAgentSdkMessageState { setStreamingNativeMessageId(scope: string, nativeMessageId: string): void { this.#streamingNativeMessageIds.set(scope, { confirmed: true, nativeId: nativeMessageId }); + this.#streamingWireUuids.delete(scope); this.#pendingAssistantStreamAnchors.delete(scope); } @@ -193,6 +243,7 @@ export class ClaudeAgentSdkMessageState { clearStreamingNativeMessageId(scope: string): void { const anchor = this.#streamingNativeMessageIds.get(scope); this.#streamingNativeMessageIds.delete(scope); + this.#streamingWireUuids.delete(scope); // The aggregated assistant envelope for this burst arrives after // message_stop; park the anchor so that envelope can bind to the streamed @@ -209,13 +260,15 @@ export class ClaudeAgentSdkMessageState { * anchor — one envelope aggregates one burst. A confirmed anchor proves * the stream's identity and the envelope's native id wins. */ - resolveAssistantMessageNativeId(scope: string, nativeMessageId: string | null): string | null { - if (nativeMessageId !== null) { - const alias = this.#assistantNativeAliases.get(nativeMessageId); - - if (alias !== undefined) { - return alias; - } + resolveAssistantMessageNativeId( + scope: string, + nativeMessageId: string | null, + wireUuid: string, + ): string { + const alias = this.#assistantWireAliases.get(wireUuid); + + if (alias !== undefined) { + return alias; } const pending = this.#pendingAssistantStreamAnchors.get(scope); @@ -223,33 +276,40 @@ export class ClaudeAgentSdkMessageState { if (pending !== undefined) { this.#pendingAssistantStreamAnchors.delete(scope); return pending.confirmed - ? (nativeMessageId ?? pending.nativeId) - : this.#bindAssistantNativeAlias(pending.nativeId, nativeMessageId); + ? nativeMessageId === pending.nativeId + ? this.#bindAssistantWireAlias(pending.nativeId, wireUuid) + : wireUuid + : this.#bindAssistantWireAlias(pending.nativeId, wireUuid); } const live = this.#streamingNativeMessageIds.get(scope); if (live === undefined) { - return nativeMessageId; + return wireUuid; } if (live.confirmed) { // The envelope arrived before message_stop; keep the confirmed anchor // so the remaining stream frames stay on the same message. - return nativeMessageId ?? live.nativeId; + const boundWireUuid = this.#streamingWireUuids.get(scope); + if ( + nativeMessageId === live.nativeId && + (boundWireUuid === undefined || boundWireUuid === wireUuid) + ) { + this.#streamingWireUuids.set(scope, wireUuid); + return this.#bindAssistantWireAlias(live.nativeId, wireUuid); + } + return wireUuid; } this.#streamingNativeMessageIds.delete(scope); - return this.#bindAssistantNativeAlias(live.nativeId, nativeMessageId); + return this.#bindAssistantWireAlias(live.nativeId, wireUuid); } - #bindAssistantNativeAlias(burstNativeId: string, nativeMessageId: string | null): string { - if (nativeMessageId !== null) { - // A replayed envelope re-presents the same native id after the anchor - // is consumed; remember the binding so it stays on the same message. - this.#assistantNativeAliases.set(nativeMessageId, burstNativeId); - } - + #bindAssistantWireAlias(burstNativeId: string, wireUuid: string): string { + // A replayed envelope re-presents the same wire uuid after the anchor is + // consumed; remember the binding so it stays on the same message. + this.#assistantWireAliases.set(wireUuid, burstNativeId); return burstNativeId; } @@ -313,6 +373,26 @@ export class ClaudeAgentSdkMessageState { return readString(readRecord(message, "message"), "id") ?? readString(message, "uuid"); } + #retractAssistantMessage(messageId: MessageId): void { + const runId = this.#assistantMessageRunIds.get(messageId); + + if (runId !== undefined) { + if (this.#activeAssistantMessageIds.get(runId) === messageId) { + this.#activeAssistantMessageIds.delete(runId); + } + + if (this.#lastCompletedAssistantMessages.get(runId)?.id === messageId) { + this.#lastCompletedAssistantMessages.delete(runId); + } + } + + this.#assistantMessageRunIds.delete(messageId); + this.#authoritativeAssistantMessageIds.delete(messageId); + this.#blockToolCallIds.delete(messageId); + this.#streamedTextMessages.delete(messageId); + this.#textByAssistantMessageId.delete(messageId); + } + #nextAssistantMessageSequence(runId: RunId): number { const sequence = (this.#assistantMessageSequences.get(runId) ?? 0) + 1; this.#assistantMessageSequences.set(runId, sequence); diff --git a/src/runtimes/claude/agent-sdk-message-translator.ts b/src/runtimes/claude/agent-sdk-message-translator.ts index 6e8cd1f..8215ab5 100644 --- a/src/runtimes/claude/agent-sdk-message-translator.ts +++ b/src/runtimes/claude/agent-sdk-message-translator.ts @@ -1,9 +1,10 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; +import { jsonValueSchema } from "../../contract"; import type { DriverEventInput } from "../../protocol/events"; import type { MessageId, RunId } from "../../protocol/id"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; -import { ClaudeAgentSdkEventWriter } from "./agent-sdk-event-writer"; +import { assertClaudeDurableEventFits, ClaudeAgentSdkEventWriter } from "./agent-sdk-event-writer"; import { isRecord, readNumber, @@ -12,12 +13,45 @@ import { stringifyForDisplay, } from "./agent-sdk-json"; import type { JsonObject } from "./agent-sdk-json"; -import { toClaudeFilesPersistedEvents } from "./agent-sdk-message-events"; +import { + aggregateClaudeModelUsage, + toClaudeFilesPersistedEvents, +} from "./agent-sdk-message-events"; +import { + claudeAssistantOutcome, + claudePermissionDenialAdvisory, + claudePermissionDenials, + claudeResultErrorDetails, + claudeToolOutcome, + isClaudeResultCancelled, + isClaudeResultRetryable, + isClaudeResultSuccessful, +} from "./agent-sdk-outcomes"; import { ClaudeAgentSdkMessageState, readClaudeSdkSessionId } from "./agent-sdk-message-state"; +import { ClaudeAgentSdkTaskEvents } from "./agent-sdk-task-events"; import { isToolUseBlock, toToolCallId, toToolCallName, toToolResultText } from "./agent-sdk-tools"; +import type { ClaudePermissionDenialAdvisory } from "./agent-sdk-outcomes"; interface ClaudeMessageTranslatorOptions { + publicToolCallId(nativeToolCallId: string): string; push(context: AgentDriverContext, reason: string, events: DriverEventInput[]): Promise; + pushTerminal( + context: AgentDriverContext, + reason: string, + closures: readonly DriverEventInput[], + terminal: DriverEventInput, + ): Promise; recordNativeSessionId(context: AgentDriverContext, sessionId: string): Promise; + replaceNativeSessionId( + context: AgentDriverContext, + previousSessionId: string, + nextSessionId: string, + ): Promise; +} + +const MAX_CLAUDE_STRUCTURED_TERMINAL_BYTES = 512 * 1_024; + +function exhaustSdkMessage(value: never): unknown { + return value; } export class ClaudeTerminalWriteError extends Error { @@ -31,39 +65,114 @@ export class ClaudeTerminalWriteError extends Error { export class ClaudeAgentSdkMessageTranslator { readonly #events: ClaudeAgentSdkEventWriter; readonly #options: ClaudeMessageTranslatorOptions; + readonly #permissionDenialAdvisories = new Map(); readonly #state = new ClaudeAgentSdkMessageState(); + readonly #tasks: ClaudeAgentSdkTaskEvents; constructor(options: ClaudeMessageTranslatorOptions) { this.#options = options; this.#events = new ClaudeAgentSdkEventWriter({ push: options.push }); + this.#tasks = new ClaudeAgentSdkTaskEvents(); } resetTurnMessageState(): void { this.#state.reset(); this.#events.resetTurnState(); + this.#tasks.reset(); + this.#permissionDenialAdvisories.clear(); } async endActiveThought(context: AgentDriverContext): Promise { - for (const thoughtId of this.#state.takeAllThoughtIds()) { + for (const [messageId, thoughtId] of this.#state.activeThoughts()) { await this.#events.endThought(context, thoughtId); + this.#state.deleteThoughtId(messageId); } } - async finishTurn(context: AgentDriverContext, toolStatus: "completed" | "failed"): Promise { - await this.endActiveThought(context); + async finishTurn( + context: AgentDriverContext, + toolStatus: "cancelled" | "completed" | "failed", + ): Promise { + if (toolStatus === "cancelled") { + for (const [messageId, thoughtId] of this.#state.activeThoughts()) { + await this.#events.cancelThought(context, thoughtId); + this.#state.deleteThoughtId(messageId); + } + } else { + await this.endActiveThought(context); + } for (const [messageId, runId] of this.#state.assistantMessages()) { - await this.#endAssistantMessage(context, runId, messageId); + if (toolStatus === "cancelled") { + await this.#events.cancelMessage(context, messageId); + this.#state.completeAssistantMessage(runId, messageId, false); + } else { + await this.#endAssistantMessage(context, runId, messageId); + } } await this.#events.finishTools(context, toolStatus); + const taskClosure = this.#tasks.prepareClosure(); + for (const [index, event] of taskClosure.events.entries()) { + taskClosure.beforePublish(index); + await this.#options.push(context, "driver.claude.tasks.finished", [event]); + taskClosure.commitAccepted(index); + } + taskClosure.commit(); + } + + async finishTurnWithTerminal( + context: AgentDriverContext, + toolStatus: "cancelled" | "completed" | "failed", + terminal: DriverEventInput, + reason: string, + ): Promise { + const closure = this.#events.prepareTurnClosure(toolStatus); + const taskClosure = this.#tasks.prepareClosure(); + await this.#options.pushTerminal( + context, + reason, + [...closure.events, ...taskClosure.events], + terminal, + ); + closure.commit(); + taskClosure.commit(); + + this.#state.clearThoughtIds(); + for (const [messageId, runId] of this.#state.assistantMessages()) { + this.#state.completeAssistantMessage(runId, messageId, toolStatus === "completed"); + } + } + + async cancelTurn(context: AgentDriverContext, runId: RunId, reason: string): Promise { + await this.finishTurnWithTerminal( + context, + "cancelled", + this.#events.runCancelled(runId, reason), + "driver.claude.turn.cancelled", + ); + } + + async failTurn( + context: AgentDriverContext, + runId: RunId, + code: string, + message: string, + ): Promise { + await this.finishTurnWithTerminal( + context, + "failed", + this.#events.runError(runId, code, message, false), + "driver.claude.turn.failed", + ); } async #endThought(context: AgentDriverContext, messageId: string): Promise { - const thoughtId = this.#state.takeThoughtId(messageId); + const thoughtId = this.#state.thoughtIdForMessage(messageId); if (thoughtId !== undefined) { await this.#events.endThought(context, thoughtId); + this.#state.deleteThoughtId(messageId); } } @@ -74,7 +183,7 @@ export class ClaudeAgentSdkMessageTranslator { ): Promise { const sessionId = readClaudeSdkSessionId(message); - if (sessionId) { + if (sessionId !== null && message.type !== "conversation_reset") { await this.#options.recordNativeSessionId(context, sessionId); } @@ -90,6 +199,10 @@ export class ClaudeAgentSdkMessageTranslator { await this.#events.pushDiagnostic(context, message); return false; } + case "conversation_reset": { + await this.#handleConversationReset(context, message); + return false; + } case "result": { await this.#handleResultMessage(context, message, runId); return true; @@ -99,7 +212,7 @@ export class ClaudeAgentSdkMessageTranslator { return false; } case "system": { - await this.#handleSystemMessage(context, message); + await this.#handleSystemMessage(context, message, runId); return false; } case "user": { @@ -110,6 +223,12 @@ export class ClaudeAgentSdkMessageTranslator { return false; } default: { + const unexpected = exhaustSdkMessage(message); + await this.#events.pushRawDiagnostic( + context, + "driver.claude.message.unknown", + isRecord(unexpected) ? unexpected : { value: String(unexpected) }, + ); return false; } } @@ -120,15 +239,19 @@ export class ClaudeAgentSdkMessageTranslator { message: Extract, runId: RunId, ): Promise { + await this.#retractWireItems(context, message.supersedes ?? []); + const outcome = claudeAssistantOutcome(message); const messageId = this.#state.assistantMessageId( runId, this.#state.resolveAssistantMessageNativeId( this.#state.streamScopeKey(runId, message), - this.#state.readNativeMessageId(message), + readString(readRecord(message, "message"), "id"), + message.uuid, ), ); const content = Array.isArray(message.message.content) ? message.message.content : []; const authoritativeText: string[] = []; + const toolCallIds: string[] = []; for (const [index, block] of content.entries()) { if (!isRecord(block)) { @@ -161,7 +284,8 @@ export class ClaudeAgentSdkMessageTranslator { } if (isToolUseBlock(block)) { - const toolCallId = toToolCallId(block, messageId, index); + const toolCallId = this.#options.publicToolCallId(toToolCallId(block, messageId, index)); + toolCallIds.push(toolCallId); if (!this.#events.hasToolStarted(toolCallId)) { // Claude content blocks are protocol-ordered; keep tool start/args/end in wire order. await this.#events.ensureToolStarted({ @@ -178,13 +302,42 @@ export class ClaudeAgentSdkMessageTranslator { } } + this.#state.bindWireAssistantMessage(message.uuid, messageId); + this.#state.bindWireToolCalls(message.uuid, toolCallIds); + if (authoritativeText.length > 0) { const text = authoritativeText.join(""); if (await this.#events.pushMessageSnapshot(context, messageId, text)) { - this.#state.markAuthoritative(messageId, text); + if (outcome.status === "completed") { + this.#state.markAuthoritative(messageId, text); + } } } + if (outcome.status === "cancelled") { + const thoughtId = this.#state.thoughtIdForMessage(messageId); + if (thoughtId !== undefined) { + await this.#events.cancelThought(context, thoughtId); + this.#state.deleteThoughtId(messageId); + } + await this.#events.ensureMessageStarted(context, messageId); + await this.#events.cancelMessage(context, messageId); + this.#state.completeAssistantMessage(runId, messageId, false); + return; + } + + if (outcome.status === "failed") { + await this.#endThought(context, messageId); + await this.#events.ensureMessageStarted(context, messageId); + await this.#events.failMessage(context, messageId, { + code: `claude.${outcome.code}`, + message: outcome.message, + retryable: outcome.retryable, + }); + this.#state.completeAssistantMessage(runId, messageId, false); + return; + } + await this.#endAssistantMessage(context, runId, messageId); } @@ -301,10 +454,8 @@ export class ClaudeAgentSdkMessageTranslator { return; } - const toolCallId = toToolCallId( - block, - messageId, - index ?? this.#state.toolCallCount(messageId), + const toolCallId = this.#options.publicToolCallId( + toToolCallId(block, messageId, index ?? this.#state.toolCallCount(messageId)), ); await this.#events.ensureToolStarted({ context, @@ -395,18 +546,24 @@ export class ClaudeAgentSdkMessageTranslator { ): Promise { const content = isRecord(message.message) ? message.message.content : null; const blocks = Array.isArray(content) ? content : []; + const structuredOutput = jsonValueSchema.safeParse(message.tool_use_result); + const toolCallIds: string[] = []; for (const block of blocks) { if (!isRecord(block)) { continue; } - const toolCallId = readString(block, "tool_use_id"); + const nativeToolCallId = readString(block, "tool_use_id"); const resultText = toToolResultText(block); - if (!toolCallId || !resultText) { + if (!nativeToolCallId || resultText === null) { continue; } + const toolCallId = this.#options.publicToolCallId(nativeToolCallId); + toolCallIds.push(toolCallId); + + const outcome = claudeToolOutcome(message, block); // Tool results are emitted in transcript order so the live state reducer can attach them deterministically. await this.#events.pushToolResult({ @@ -417,10 +574,19 @@ export class ClaudeAgentSdkMessageTranslator { this.#state.activeAssistantMessageId(runId) ?? this.#state.lastCompletedAssistantMessageId(runId) ?? this.#state.assistantMessageId(runId, null), - status: block["is_error"] === true ? "failed" : "completed", + ...(outcome.nonExecutionKind === undefined + ? {} + : { nonExecutionKind: outcome.nonExecutionKind }), + status: outcome.status, + ...(structuredOutput.success ? { structuredOutput: structuredOutput.data } : {}), toolCallId, + ...(outcome.userFeedback === undefined ? {} : { userFeedback: outcome.userFeedback }), }); } + + if (message.uuid !== undefined) { + this.#state.bindWireToolCalls(message.uuid, toolCallIds); + } } async #endAssistantMessage( @@ -435,33 +601,196 @@ export class ClaudeAgentSdkMessageTranslator { async #handleSystemMessage( context: AgentDriverContext, message: Extract, + runId: RunId, ): Promise { - if (message.subtype === "init") { - await this.#options.recordNativeSessionId(context, message.session_id); - await this.#events.pushSessionInfoUpdated(context); - context.logger.info("driver.claude.session.initialized", { - mcpServerCount: message.mcp_servers.length, - model: message.model, - nativeSessionIdPresent: true, - toolCount: message.tools.length, - }); + switch (message.subtype) { + case "init": { + await this.#events.pushSessionInfoUpdated(context); + context.logger.info("driver.claude.session.initialized", { + mcpServerCount: message.mcp_servers.length, + model: message.model, + nativeSessionIdPresent: true, + toolCount: message.tools.length, + }); + return; + } + case "files_persisted": { + await this.#handleFilesPersisted(context, message); + return; + } + case "informational": { + const nativeToolCallId = message.tool_use_id; + await this.#pushStandaloneMessage(context, runId, message.uuid, message.content, { + level: message.level, + ...(message.prevent_continuation === undefined + ? {} + : { preventContinuation: message.prevent_continuation }), + subtype: message.subtype, + ...(nativeToolCallId === undefined + ? {} + : { toolCallId: this.#options.publicToolCallId(nativeToolCallId) }), + }); + return; + } + case "local_command_output": { + await this.#pushStandaloneMessage(context, runId, message.uuid, message.content, { + subtype: message.subtype, + }); + return; + } + case "mirror_error": { + await this.#events.pushRawDiagnostic( + context, + "driver.claude.mirror_error", + { + errorBytes: Buffer.byteLength(message.error, "utf8"), + kind: "claude.mirror_error", + }, + { message: "Claude transcript mirror write failed.", severity: "error" }, + ); + return; + } + case "model_refusal_fallback": { + await this.#retractWireItems(context, message.retracted_message_uuids ?? []); + await this.#events.pushDiagnostic(context, message); + return; + } + case "permission_denied": { + const denial = claudePermissionDenialAdvisory(message); + if (denial !== null) { + const toolCallId = this.#options.publicToolCallId(denial.toolCallId); + this.#permissionDenialAdvisories.set(`${runId}:${toolCallId}`, { + ...denial, + toolCallId, + }); + } + return; + } + case "api_retry": + case "commands_changed": + case "compact_boundary": + case "control_request_progress": + case "elicitation_complete": + case "hook_progress": + case "hook_response": + case "hook_started": + case "memory_recall": + case "model_refusal_no_fallback": + case "notification": + case "plugin_install": + case "session_state_changed": + case "status": + case "task_notification": + case "task_progress": + case "task_started": + case "task_updated": + case "thinking_tokens": + case "worker_shutting_down": { + await this.#events.pushDiagnostic(context, message); + return; + } + case "background_tasks_changed": { + await this.#handleBackgroundTasksChanged(context, message); + return; + } + default: { + const unexpected = exhaustSdkMessage(message); + await this.#events.pushRawDiagnostic( + context, + "driver.claude.system.unknown", + isRecord(unexpected) ? unexpected : { value: String(unexpected) }, + ); + } + } + } + + async #handleBackgroundTasksChanged( + context: AgentDriverContext, + message: Extract, + ): Promise { + const projection = this.#tasks.prepare(message); + for (const [index, event] of projection.events.entries()) { + projection.beforePublish(index); + await this.#options.push(context, "driver.claude.task.updated", [event]); + projection.commitAccepted(index); + } + projection.commit(); + } + + async #retractWireItems( + context: AgentDriverContext, + wireUuids: readonly string[], + ): Promise { + for (const wireUuid of wireUuids) { + const { messageId, toolCallIds } = this.#state.wireItems(wireUuid); + + if (messageId !== null) { + const thoughtId = this.#state.thoughtIdForMessage(messageId); + if (thoughtId !== undefined) { + await this.#events.cancelThought(context, thoughtId); + this.#state.deleteThoughtId(messageId); + } + await this.#events.retractMessage(context, messageId); + this.#state.commitWireMessageRetraction(wireUuid, messageId); + } + + for (const toolCallId of toolCallIds) { + await this.#events.retractTool(context, toolCallId); + } + this.#state.commitWireToolRetractions(wireUuid); + } + } + + async #pushStandaloneMessage( + context: AgentDriverContext, + runId: RunId, + nativeMessageId: string, + content: string, + metadata: JsonObject, + ): Promise { + if (content.length === 0) { + await this.#events.pushRawDiagnostic( + context, + `driver.claude.${String(metadata["subtype"] ?? "message")}`, + { content, ...metadata }, + { message: "Claude emitted an empty display message.", severity: "warn" }, + ); return; } - if (message.subtype === "files_persisted") { - await this.#handleFilesPersisted(context, message); + const messageId = this.#state.auxiliaryMessageId(runId, nativeMessageId); + if (await this.#events.pushMessageSnapshot(context, messageId, content, metadata)) { + await this.#events.endMessage(context, messageId); } } - async #handleFilesPersisted( + async #handleConversationReset( context: AgentDriverContext, - message: Extract, + message: Extract, ): Promise { - await this.#options.push( + await this.finishTurn(context, "cancelled"); + await this.#options.replaceNativeSessionId( context, - "driver.claude.files.persisted", - toClaudeFilesPersistedEvents(message), + message.session_id, + message.new_conversation_id, ); + this.resetTurnMessageState(); + await this.#events.pushSessionInfoUpdated(context, true); + } + + async #handleFilesPersisted( + context: AgentDriverContext, + message: Extract, + ): Promise { + const events = toClaudeFilesPersistedEvents(message); + for (const event of events) { + assertClaudeDurableEventFits( + event, + "claude.files_persisted_too_large", + "file persistence event", + ); + } + await this.#options.push(context, "driver.claude.files.persisted", events); } async #handleResultMessage( @@ -469,16 +798,120 @@ export class ClaudeAgentSdkMessageTranslator { message: Extract, runId: RunId, ): Promise { - await this.finishTurn(context, message.subtype === "success" ? "completed" : "failed"); + const successful = isClaudeResultSuccessful(message); + const cancelled = isClaudeResultCancelled(message); + + for (const denial of claudePermissionDenials(message)) { + const toolCallId = this.#options.publicToolCallId(denial.toolCallId); + const advisory = this.#permissionDenialAdvisories.get(`${runId}:${toolCallId}`); + const messageId = + this.#events.toolParentMessageId(toolCallId) ?? + this.#state.activeAssistantMessageId(runId) ?? + this.#state.lastCompletedAssistantMessageId(runId) ?? + null; + await this.#events.ensureToolStarted({ + context, + ...(messageId === null ? {} : { parentMessageId: messageId }), + toolCallId, + toolCallName: denial.name, + }); + await this.#events.pushToolResult({ + ...(advisory?.agentId === undefined ? {} : { agentId: advisory.agentId }), + authoritative: true, + content: advisory?.message ?? denial.message, + context, + ...(advisory?.decisionReason === undefined + ? {} + : { decisionReason: advisory.decisionReason }), + ...(advisory?.decisionReasonType === undefined + ? {} + : { decisionReasonType: advisory.decisionReasonType }), + ...(messageId === null ? {} : { messageId }), + rawInput: stringifyForDisplay(denial.input), + status: "failed", + toolCallId, + toolCallName: denial.name, + }); + this.#permissionDenialAdvisories.delete(`${runId}:${toolCallId}`); + } + await this.#events.pushUsage( context, - isRecord(message.usage) ? message.usage : null, + aggregateClaudeModelUsage(message.modelUsage), message.total_cost_usd, ); - if (message.subtype === "success") { + if (cancelled) { + try { + await this.finishTurnWithTerminal( + context, + "cancelled", + this.#events.runCancelled(runId, message.terminal_reason ?? "provider.aborted"), + "driver.claude.turn.cancelled", + ); + } catch (error) { + throw new ClaudeTerminalWriteError(error); + } + return; + } + + if (message.subtype === "success" && successful) { const resultText = isRecord(message) ? readString(message, "result") : null; - if (resultText !== null && resultText.length > 0 && !this.#state.hasAssistantText(runId)) { + const structuredOutput = + message.structured_output === undefined + ? undefined + : jsonValueSchema.safeParse(message.structured_output); + + if (structuredOutput !== undefined && !structuredOutput.success) { + try { + await this.finishTurnWithTerminal( + context, + "failed", + this.#events.runError( + runId, + "claude.invalid_structured_output", + "Claude Agent SDK returned a non-JSON structured output.", + false, + ), + "driver.claude.turn.failed", + ); + } catch (error) { + throw new ClaudeTerminalWriteError(error); + } + return; + } + + const completedTerminal = this.#events.runFinished(runId, null, structuredOutput?.data); + const structuredOutputTooLarge = + structuredOutput?.success === true && + Buffer.byteLength(JSON.stringify(completedTerminal), "utf8") > + MAX_CLAUDE_STRUCTURED_TERMINAL_BYTES; + + if (structuredOutputTooLarge) { + try { + await this.finishTurnWithTerminal( + context, + "failed", + this.#events.runError( + runId, + "claude.structured_output_too_large", + "Claude Agent SDK structured output exceeds the runtime terminal event limit.", + false, + ), + "driver.claude.turn.failed", + ); + } catch (error) { + throw new ClaudeTerminalWriteError(error); + } + return; + } + + if ( + structuredOutput === undefined && + resultText !== null && + resultText.length > 0 && + !this.#state.hasAssistantText(runId) + ) { const messageId = this.#state.assistantMessageId(runId, null); if (await this.#events.pushMessageSnapshot(context, messageId, resultText)) { this.#state.markAuthoritative(messageId, resultText); @@ -490,10 +923,17 @@ export class ClaudeAgentSdkMessageTranslator { // resume can omit every assistant frame, so the result is materialized // above only when no competing text candidate exists. const finalMessage = - resultText === null ? null : this.#state.resolveFinalAssistantSnapshot(runId, resultText); + structuredOutput !== undefined || resultText === null + ? null + : this.#state.resolveFinalAssistantSnapshot(runId, resultText); try { - await this.#events.pushRunFinished(context, runId, finalMessage); + await this.finishTurnWithTerminal( + context, + "completed", + this.#events.runFinished(runId, finalMessage, structuredOutput?.data), + "driver.claude.turn.completed", + ); } catch (error) { throw new ClaudeTerminalWriteError(error); } @@ -501,11 +941,19 @@ export class ClaudeAgentSdkMessageTranslator { } try { - await this.#events.pushRunError( + await this.finishTurnWithTerminal( context, - runId, - `claude.${message.subtype}`, - message.errors.join("\n") || "Claude Agent SDK turn failed.", + "failed", + this.#events.runError( + runId, + message.subtype === "success" ? "claude.api_error" : `claude.${message.subtype}`, + message.subtype === "success" + ? message.result || "Claude Agent SDK API request failed." + : message.errors.join("\n") || "Claude Agent SDK turn failed.", + isClaudeResultRetryable(message), + claudeResultErrorDetails(message), + ), + "driver.claude.turn.failed", ); } catch (error) { throw new ClaudeTerminalWriteError(error); diff --git a/src/runtimes/claude/agent-sdk-outcomes.ts b/src/runtimes/claude/agent-sdk-outcomes.ts new file mode 100644 index 0000000..9a64df6 --- /dev/null +++ b/src/runtimes/claude/agent-sdk-outcomes.ts @@ -0,0 +1,183 @@ +import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; + +import { isRecord, readString } from "./agent-sdk-json"; +import type { JsonObject } from "./agent-sdk-json"; + +const CANCELLED_TOOL_OUTCOMES = new Set(["cancelled", "interrupted"]); +const NON_EXECUTION_KINDS = new Set([ + "automode-blocked", + "automode-parsing-error", + "automode-unavailable", + "cancelled", + "interrupted", + "permission-rule", + "user-rejected", +]); +const RETRYABLE_ASSISTANT_ERRORS = new Set(["overloaded", "rate_limit", "server_error"]); + +export type ClaudeAssistantOutcome = + | { readonly status: "cancelled" } + | { readonly status: "completed" } + | { + readonly code: string; + readonly message: string; + readonly retryable: boolean; + readonly status: "failed"; + }; + +export interface ClaudePermissionDenial { + readonly input: JsonObject; + readonly message: string; + readonly name: string; + readonly toolCallId: string; +} + +export interface ClaudePermissionDenialAdvisory { + readonly agentId?: string; + readonly decisionReason?: string; + readonly decisionReasonType?: string; + readonly message: string; + readonly toolCallId: string; +} + +export interface ClaudeToolOutcome { + readonly nonExecutionKind?: string; + readonly status: "cancelled" | "completed" | "failed"; + readonly userFeedback?: string; +} + +export function claudeAssistantOutcome( + message: Extract, +): ClaudeAssistantOutcome { + if (message.aborted === true) { + return { status: "cancelled" }; + } + + if (message.error === undefined) { + return { status: "completed" }; + } + + return { + code: message.error, + message: `Assistant message failed: ${message.error}.`, + retryable: RETRYABLE_ASSISTANT_ERRORS.has(message.error), + status: "failed", + }; +} + +export function claudePermissionDenials( + message: Extract, +): ClaudePermissionDenial[] { + const denials = Array.isArray(message.permission_denials) ? message.permission_denials : []; + + return denials.flatMap((entry) => { + if (!isRecord(entry)) { + return []; + } + + const name = readString(entry, "tool_name"); + const toolCallId = readString(entry, "tool_use_id"); + const input = entry["tool_input"]; + + if (name === null || toolCallId === null || !isRecord(input)) { + return []; + } + + return [ + { + input, + message: `Permission denied for ${name}; the tool was not executed.`, + name, + toolCallId, + }, + ]; + }); +} + +export function claudePermissionDenialAdvisory( + message: Extract, +): ClaudePermissionDenialAdvisory | null { + if (message.subtype !== "permission_denied") { + return null; + } + + return { + ...(message.agent_id === undefined ? {} : { agentId: message.agent_id }), + ...(message.decision_reason === undefined ? {} : { decisionReason: message.decision_reason }), + ...(message.decision_reason_type === undefined + ? {} + : { decisionReasonType: message.decision_reason_type }), + message: message.message, + toolCallId: message.tool_use_id, + }; +} + +export function claudeResultErrorDetails( + message: Extract, +): Record | undefined { + const apiErrorStatus = message.subtype === "success" ? message.api_error_status : undefined; + + if (apiErrorStatus === undefined && message.terminal_reason === undefined) { + return undefined; + } + + return { + ...(apiErrorStatus === undefined ? {} : { apiErrorStatus }), + ...(message.terminal_reason === undefined ? {} : { terminalReason: message.terminal_reason }), + }; +} + +export function isClaudeResultRetryable(message: Extract): boolean { + const apiErrorStatus = message.subtype === "success" ? message.api_error_status : undefined; + + if (apiErrorStatus !== null && apiErrorStatus !== undefined) { + return ( + apiErrorStatus === 408 || + apiErrorStatus === 409 || + apiErrorStatus === 429 || + apiErrorStatus >= 500 + ); + } + + return ( + message.terminal_reason === "api_error" || + message.terminal_reason === "model_error" || + message.terminal_reason === "blocking_limit" || + message.terminal_reason === "rapid_refill_breaker" + ); +} + +export function isClaudeResultSuccessful( + message: Extract, +): boolean { + return message.subtype === "success" && !message.is_error; +} + +export function claudeToolOutcome( + message: Extract, + block: JsonObject, +): ClaudeToolOutcome { + const toolCallId = readString(block, "tool_use_id"); + const rawMetadata = (message as unknown as JsonObject)["tool_result_meta"]; + const metadata: unknown[] = Array.isArray(rawMetadata) ? rawMetadata : []; + const match = metadata.find((entry) => isRecord(entry) && readString(entry, "id") === toolCallId); + const metadataEntry = isRecord(match) ? match : null; + const nonExecutionKind = readString(metadataEntry, "non_execution_kind"); + + if (nonExecutionKind !== null && NON_EXECUTION_KINDS.has(nonExecutionKind)) { + const userFeedback = readString(metadataEntry, "user_feedback"); + return { + nonExecutionKind, + status: CANCELLED_TOOL_OUTCOMES.has(nonExecutionKind) ? "cancelled" : "failed", + ...(userFeedback === null ? {} : { userFeedback }), + }; + } + + return { status: block["is_error"] === true ? "failed" : "completed" }; +} + +export function isClaudeResultCancelled(message: Extract): boolean { + return ( + message.terminal_reason === "aborted_streaming" || message.terminal_reason === "aborted_tools" + ); +} diff --git a/src/runtimes/claude/agent-sdk-prewarm.ts b/src/runtimes/claude/agent-sdk-prewarm.ts index 1e4801e..0d75524 100644 --- a/src/runtimes/claude/agent-sdk-prewarm.ts +++ b/src/runtimes/claude/agent-sdk-prewarm.ts @@ -21,6 +21,7 @@ export interface ClaudeAgentSdkPrewarmOptions { readonly createQueryOptions: typeof createClaudeQueryOptions; readonly getNativeSessionId: () => string | null; readonly payload: DriverStartInput; + readonly publicToolCallId: (nativeToolCallId: string) => string; readonly startup: (input: { options: Awaited>; }) => Promise; @@ -41,6 +42,7 @@ export class ClaudeAgentSdkPrewarm { readonly #createQueryOptions: typeof createClaudeQueryOptions; readonly #getNativeSessionId: () => string | null; readonly #payload: DriverStartInput; + readonly #publicToolCallId: (nativeToolCallId: string) => string; readonly #startup: ClaudeAgentSdkPrewarmOptions["startup"]; #failure: { readonly error: unknown } | null = null; #state: ClaudePrewarmState | null = null; @@ -51,6 +53,7 @@ export class ClaudeAgentSdkPrewarm { this.#createQueryOptions = options.createQueryOptions; this.#getNativeSessionId = options.getNativeSessionId; this.#payload = options.payload; + this.#publicToolCallId = options.publicToolCallId; this.#startup = options.startup; } @@ -163,6 +166,7 @@ export class ClaudeAgentSdkPrewarm { payload: this.#payload, permissionTasks: state.permissionTasks, processTasks: state.processTasks, + publicToolCallId: this.#publicToolCallId, }); if (this.#stopped || abortController.signal.aborted || this.#state !== state) { diff --git a/src/runtimes/claude/agent-sdk-query-options.ts b/src/runtimes/claude/agent-sdk-query-options.ts index 1610335..1b389e5 100644 --- a/src/runtimes/claude/agent-sdk-query-options.ts +++ b/src/runtimes/claude/agent-sdk-query-options.ts @@ -53,6 +53,7 @@ const CLAUDE_PROVIDER_OPTION_KEYS = new Set([ function createCanUseTool( context: AgentDriverContext, permissionTasks: Set>, + publicToolCallId: (nativeToolCallId: string) => string, ): CanUseTool { return (toolName, input, options): Promise => { const task = (async () => { @@ -67,10 +68,19 @@ function createCanUseTool( const decision = await context.ports.permission.request( { + ...(options.agentID === undefined ? {} : { agentId: options.agentID }), + ...(options.blockedPath === undefined ? {} : { blockedPath: options.blockedPath }), + ...(options.decisionReason === undefined + ? {} + : { decisionReason: options.decisionReason }), + ...(options.description === undefined ? {} : { description: options.description }), + ...(options.matchedAskRule === undefined + ? {} + : { matchedAskRule: options.matchedAskRule }), rawInput: stringifyForDisplay(input), requestId: options.requestId, title: options.title ?? options.displayName ?? `Approve ${toolName}`, - toolCallId: options.toolUseID, + toolCallId: publicToolCallId(options.toolUseID), toolKind: toolName, }, options.signal, @@ -165,6 +175,7 @@ export async function createClaudeQueryOptions(input: { payload: DriverStartInput; permissionTasks?: Set>; processTasks?: Set>; + publicToolCallId?: (nativeToolCallId: string) => string; }): Promise { const claudeConfigDir = resolveClaudeConfigDir(input.payload); await mkdir(claudeConfigDir, { recursive: true }); @@ -174,7 +185,11 @@ export async function createClaudeQueryOptions(input: { const options: ClaudeQueryOptions = { abortController: input.abortController, additionalDirectories: input.payload.execution.session.additionalDirectories, - canUseTool: createCanUseTool(input.context, input.permissionTasks ?? new Set()), + canUseTool: createCanUseTool( + input.context, + input.permissionTasks ?? new Set(), + input.publicToolCallId ?? ((nativeToolCallId) => nativeToolCallId), + ), cwd: input.payload.execution.session.cwd, env: toClaudeEnv(input.payload, claudeConfigDir), includePartialMessages: true, diff --git a/src/runtimes/claude/agent-sdk-resume.ts b/src/runtimes/claude/agent-sdk-resume.ts index 78e71da..10c2615 100644 --- a/src/runtimes/claude/agent-sdk-resume.ts +++ b/src/runtimes/claude/agent-sdk-resume.ts @@ -1,5 +1,19 @@ import type { DriverStartInput } from "../../protocol/start"; +const MAX_CLAUDE_NATIVE_SESSION_ID_BYTES = 256; + +export function requireClaudeNativeSessionId(sessionId: string): string { + const bytes = Buffer.byteLength(sessionId, "utf8"); + + if (sessionId.trim().length === 0 || bytes > MAX_CLAUDE_NATIVE_SESSION_ID_BYTES) { + throw new RangeError( + `Claude native session ID must contain 1-${String(MAX_CLAUDE_NATIVE_SESSION_ID_BYTES)} UTF-8 bytes (received ${String(bytes)}).`, + ); + } + + return sessionId; +} + export function readClaudeNativeResumeSessionId(payload: DriverStartInput): string | null { const { nativeResumeRef } = payload.execution.session; @@ -14,5 +28,5 @@ export function readClaudeNativeResumeSessionId(payload: DriverStartInput): stri throw new Error("Claude runtime received an incompatible native resume ref."); } - return nativeResumeRef.value; + return requireClaudeNativeSessionId(nativeResumeRef.value); } diff --git a/src/runtimes/claude/agent-sdk-task-events.ts b/src/runtimes/claude/agent-sdk-task-events.ts new file mode 100644 index 0000000..2b31ce3 --- /dev/null +++ b/src/runtimes/claude/agent-sdk-task-events.ts @@ -0,0 +1,206 @@ +import { createHash } from "node:crypto"; + +import type { SDKBackgroundTasksChangedMessage } from "@anthropic-ai/claude-agent-sdk"; + +import type { DriverEventInput } from "../../protocol/events"; +import { assertClaudeDurableEventFits } from "./agent-sdk-event-writer"; + +interface ClaudeBackgroundTask { + readonly taskType?: string | undefined; + readonly title?: string | undefined; +} + +export interface ClaudeTaskProjection { + readonly beforePublish: (index: number) => void; + readonly commit: () => void; + readonly commitAccepted: (index: number) => void; + readonly events: readonly DriverEventInput[]; +} + +const MAX_CLAUDE_TASK_ID_BYTES = 256; +const MAX_CLAUDE_BACKGROUND_TASK_ENTRIES = 1_024; +const MAX_CLAUDE_TASK_TEXT_LENGTH = 4_096; +const MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS = 256; +const EMPTY_CLAUDE_TASK_PROJECTION: ClaudeTaskProjection = { + beforePublish: () => {}, + commit: () => {}, + commitAccepted: () => {}, + events: [], +}; + +function rejectedTaskSnapshot(code: string, taskCount: number): ClaudeTaskProjection { + const event: DriverEventInput = { + delivery: "best_effort", + kind: "diagnostic.reported", + payload: { + code, + details: { taskCount }, + message: "Claude background task snapshot exceeded the supported task bound.", + severity: "warn", + source: "claude", + }, + visibility: "owner_debug", + }; + assertClaudeDurableEventFits(event, code, "background task snapshot diagnostic"); + return { ...EMPTY_CLAUDE_TASK_PROJECTION, events: [event] }; +} + +function publicTaskId(nativeTaskId: string): string | null { + const bytes = Buffer.byteLength(nativeTaskId, "utf8"); + if (bytes === 0) { + return null; + } + return bytes <= MAX_CLAUDE_TASK_ID_BYTES + ? nativeTaskId + : `claude-task:${createHash("sha256").update(nativeTaskId).digest("hex")}`; +} + +function boundedTaskText(value: string): string | undefined { + const text = value.slice(0, MAX_CLAUDE_TASK_TEXT_LENGTH); + const finalCodeUnit = text.charCodeAt(text.length - 1); + const bounded = finalCodeUnit >= 0xd800 && finalCodeUnit <= 0xdbff ? text.slice(0, -1) : text; + return bounded.length === 0 ? undefined : bounded; +} + +function taskEvent(taskId: string, payload: Record): DriverEventInput { + const event: DriverEventInput = { + delivery: "lossless", + kind: "agent.task.updated", + payload: { taskId, ...payload }, + }; + assertClaudeDurableEventFits(event, "claude.task_update_too_large", "task update"); + return event; +} + +export class ClaudeAgentSdkTaskEvents { + #backgroundTasks = new Map(); + + reset(): void { + this.#backgroundTasks.clear(); + } + + prepare(message: SDKBackgroundTasksChangedMessage): ClaudeTaskProjection { + if (message.tasks.length > MAX_CLAUDE_BACKGROUND_TASK_ENTRIES) { + return rejectedTaskSnapshot( + "claude.background_tasks_snapshot_too_large", + message.tasks.length, + ); + } + + const next = new Map(); + for (const task of message.tasks) { + if (task.ambient === true) { + continue; + } + if (next.size >= MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS) { + return rejectedTaskSnapshot( + "claude.visible_background_tasks_too_many", + message.tasks.length, + ); + } + const taskId = publicTaskId(task.task_id); + if (taskId !== null) { + next.set(taskId, { + taskType: boundedTaskText(task.task_type), + title: boundedTaskText(task.description), + }); + } + } + + const changes: Array<{ + readonly optimistic: boolean; + readonly task: ClaudeBackgroundTask | null; + readonly taskId: string; + }> = []; + const events: DriverEventInput[] = []; + const working = new Map(this.#backgroundTasks); + const add = ( + event: DriverEventInput, + taskId: string, + task: ClaudeBackgroundTask | null, + optimistic = false, + ): void => { + events.push(event); + changes.push({ optimistic, task, taskId }); + }; + + for (const taskId of this.#backgroundTasks.keys()) { + if (!next.has(taskId)) { + working.delete(taskId); + add(taskEvent(taskId, { active: false }), taskId, null); + } + } + + for (const [taskId, task] of next) { + const previous = working.get(taskId); + const normalizedTask = { + taskType: task.taskType ?? previous?.taskType, + title: task.title ?? previous?.title, + }; + next.set(taskId, normalizedTask); + if (previous === undefined) { + working.set(taskId, normalizedTask); + add( + taskEvent(taskId, { + active: true, + ...(normalizedTask.taskType === undefined ? {} : { taskType: normalizedTask.taskType }), + ...(normalizedTask.title === undefined ? {} : { title: normalizedTask.title }), + }), + taskId, + normalizedTask, + true, + ); + continue; + } + + const patch: Record = {}; + if (normalizedTask.taskType !== undefined && previous.taskType !== normalizedTask.taskType) { + patch["taskType"] = normalizedTask.taskType; + } + if (normalizedTask.title !== undefined && previous.title !== normalizedTask.title) { + patch["title"] = normalizedTask.title; + } + if (Object.keys(patch).length > 0) { + working.set(taskId, normalizedTask); + add(taskEvent(taskId, patch), taskId, normalizedTask); + } + } + + return { + beforePublish: (index) => { + const change = changes[index]; + if (change?.optimistic === true && change.task !== null) { + this.#backgroundTasks.set(change.taskId, change.task); + } + }, + commit: () => { + this.#backgroundTasks = next; + }, + commitAccepted: (index) => { + const change = changes[index]; + if (change !== undefined) { + if (change.task === null) this.#backgroundTasks.delete(change.taskId); + else this.#backgroundTasks.set(change.taskId, change.task); + } + }, + events, + }; + } + + prepareClosure(): ClaudeTaskProjection { + const taskIds = [...this.#backgroundTasks.keys()]; + const events = taskIds.map((taskId) => taskEvent(taskId, { active: false })); + + return { + beforePublish: () => {}, + commit: () => { + this.#backgroundTasks.clear(); + }, + commitAccepted: (index) => { + const taskId = taskIds[index]; + if (taskId !== undefined) this.#backgroundTasks.delete(taskId); + }, + events, + }; + } +} diff --git a/src/runtimes/claude/agent-sdk-tool-id.ts b/src/runtimes/claude/agent-sdk-tool-id.ts new file mode 100644 index 0000000..0a6c71f --- /dev/null +++ b/src/runtimes/claude/agent-sdk-tool-id.ts @@ -0,0 +1,35 @@ +import { createHash } from "node:crypto"; + +import { createDriverId } from "../../protocol/id"; + +const MAX_CLAUDE_PUBLIC_TOOL_CALL_ID_BYTES = 256; + +function claudeNativeToolCallIdKey(nativeToolCallId: string): string { + return Buffer.byteLength(nativeToolCallId, "utf8") <= MAX_CLAUDE_PUBLIC_TOOL_CALL_ID_BYTES + ? nativeToolCallId + : createHash("sha256").update(nativeToolCallId).digest("hex"); +} + +export class ClaudePublicToolCallIdState { + readonly #publicIds = new Map(); + + publicId(nativeToolCallId: string): string { + if (Buffer.byteLength(nativeToolCallId, "utf8") <= MAX_CLAUDE_PUBLIC_TOOL_CALL_ID_BYTES) { + return nativeToolCallId; + } + + const key = claudeNativeToolCallIdKey(nativeToolCallId); + const existing = this.#publicIds.get(key); + if (existing !== undefined) { + return existing; + } + + const publicId = createDriverId(); + this.#publicIds.set(key, publicId); + return publicId; + } + + reset(): void { + this.#publicIds.clear(); + } +} diff --git a/src/runtimes/claude/contract-adapter.ts b/src/runtimes/claude/contract-adapter.ts deleted file mode 100644 index 5e79b93..0000000 --- a/src/runtimes/claude/contract-adapter.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { PermissionResult, SDKMessage } from "@anthropic-ai/claude-agent-sdk"; - -import type { InteractionResolution, Run } from "../../contract"; -import { createDriverId } from "../../protocol/id"; -import { ContractProjection, type ContractProjectionOptions } from "../contract-projection"; -import { isRecord, readString } from "./agent-sdk-json"; -import { ClaudeContractPermissions, type ClaudePermissionOptions } from "./contract-permissions"; -import { ClaudeContractTranscript } from "./contract-transcript"; - -const DEFAULT_INTERACTION_TIMEOUT_MS = 5 * 60 * 1_000; -const DEFAULT_MAX_PENDING_PERMISSION_BYTES = 8 * 1_024 * 1_024; -const DEFAULT_MAX_TOOL_INPUT_BYTES = 1_024 * 1_024; - -export interface ClaudeContractAdapterOptions extends ContractProjectionOptions { - readonly createId?: (() => string) | undefined; - readonly interactionTimeoutMs?: number | undefined; - readonly maxPendingPermissionBytes?: number | undefined; - readonly maxToolInputBytes?: number | undefined; - readonly nativeSessionId?: string | undefined; -} - -export class ClaudeContractAdapter { - #disposed = false; - readonly #finishingRuns = new Set(); - #nativeSessionId: string | null; - readonly #permissions: ClaudeContractPermissions; - readonly #projection: ContractProjection; - readonly #transcript: ClaudeContractTranscript; - - constructor(options: ClaudeContractAdapterOptions) { - const createId = options.createId ?? createDriverId; - const interactionTimeoutMs = options.interactionTimeoutMs ?? DEFAULT_INTERACTION_TIMEOUT_MS; - const maxPendingPermissionBytes = - options.maxPendingPermissionBytes ?? DEFAULT_MAX_PENDING_PERMISSION_BYTES; - const maxToolInputBytes = options.maxToolInputBytes ?? DEFAULT_MAX_TOOL_INPUT_BYTES; - this.#nativeSessionId = options.nativeSessionId ?? null; - this.#projection = new ContractProjection(options); - this.#transcript = new ClaudeContractTranscript({ - createId, - maxToolInputBytes, - onRunReleased: (runId) => this.#releaseRun(runId), - projection: this.#projection, - }); - this.#permissions = new ClaudeContractPermissions({ - createId, - interactionTimeoutMs, - isRunFinishing: (runId) => this.#finishingRuns.has(runId), - maxPendingPermissionBytes, - projection: this.#projection, - transcript: this.#transcript, - }); - - if ( - [interactionTimeoutMs, maxPendingPermissionBytes, maxToolInputBytes].some( - (value) => !Number.isSafeInteger(value) || value < 1, - ) - ) { - throw new RangeError("Claude Agent SDK limits must be finite and positive."); - } - - if (this.#nativeSessionId !== null && this.#nativeSessionId.trim().length === 0) { - throw new Error("Claude Contract adapter requires a non-empty native session ID."); - } - } - - attachRun(run: Run): void { - this.#assertActive(); - this.#projection.attachRun(run); - } - - async handleMessage(message: SDKMessage, runId: string): Promise { - this.#assertActive(); - this.#assertNativeSession(message); - - if (this.#finishingRuns.has(runId) || this.#projection.run(runId)?.status !== "active") { - return false; - } - - if (message.type !== "result") { - return this.#transcript.handleMessage(message, runId); - } - - this.#finishingRuns.add(runId); - try { - return await this.#transcript.handleMessage(message, runId); - } catch (error) { - if (this.#projection.run(runId)?.status === "active") { - this.#finishingRuns.delete(runId); - } - throw error; - } - } - - async openPermission( - runId: string, - toolName: string, - input: Record, - options: ClaudePermissionOptions, - ): Promise { - this.#assertActive(); - return this.#permissions.openPermission(runId, toolName, input, options); - } - - async resolveInteraction( - interactionId: string, - resolution: InteractionResolution, - ): Promise { - this.#assertActive(); - return this.#permissions.resolveInteraction(interactionId, resolution); - } - - dispose(): void { - this.#disposed = true; - this.#finishingRuns.clear(); - this.#permissions.dispose(); - this.#transcript.dispose(); - this.#nativeSessionId = null; - this.#projection.dispose(); - } - - #assertNativeSession(message: SDKMessage): void { - const sessionId = isRecord(message) ? readString(message, "session_id") : null; - - if (sessionId === null) { - return; - } - - if (sessionId.trim().length === 0) { - throw new Error("Claude Agent SDK message has an empty native session ID."); - } - - if (this.#nativeSessionId === null) { - this.#nativeSessionId = sessionId; - return; - } - - if (sessionId !== this.#nativeSessionId) { - throw new Error("Claude Agent SDK message belongs to a different native session."); - } - } - - #releaseRun(runId: string): void { - this.#finishingRuns.delete(runId); - this.#transcript.releaseRun(runId); - this.#permissions.releaseRun(runId); - } - - #assertActive(): void { - if (this.#disposed) { - throw new Error("Claude Contract adapter is disposed."); - } - } -} diff --git a/src/runtimes/claude/contract-items.ts b/src/runtimes/claude/contract-items.ts deleted file mode 100644 index 2bbb930..0000000 --- a/src/runtimes/claude/contract-items.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; - -import type { ContentBlock, TokenUsage, ToolItem } from "../../contract"; -import { asJsonValue } from "../contract-adapter-meta"; -import { - isRecord, - readRecord, - readString, - sumTokenCounts, - toCostAmount, - toTokenCount, -} from "./agent-sdk-json"; - -export function toContentBlocks(value: unknown): ContentBlock[] { - const values = Array.isArray(value) ? value : [value]; - - return values.flatMap((entry) => { - if (typeof entry === "string") { - return entry.length === 0 ? [] : [{ text: entry, type: "text" }]; - } - - if (!isRecord(entry)) { - const json = asJsonValue(entry); - return json === undefined ? [] : [{ type: "json", value: json }]; - } - - if (entry["type"] === "text") { - const text = readString(entry, "text"); - return text === null ? [] : [{ text, type: "text" }]; - } - - const source = readRecord(entry, "source"); - - if (entry["type"] === "image" && source?.["type"] === "base64") { - const data = readString(source, "data"); - const mediaType = readString(source, "media_type"); - return data === null || mediaType === null ? [] : [{ data, mediaType, type: "inline_blob" }]; - } - - if (entry["type"] === "image" && source?.["type"] === "url") { - const uri = readString(source, "url"); - return uri !== null && URL.canParse(uri) ? [{ type: "resource_link", uri }] : []; - } - - const json = asJsonValue(entry); - return json === undefined ? [] : [{ type: "json", value: json }]; - }); -} - -export function toolCategory(name: string): ToolItem["category"] { - const normalized = name.toLowerCase(); - - if (normalized === "read") { - return "read"; - } - - if (["edit", "multiedit", "notebookedit", "write"].includes(normalized)) { - return "edit"; - } - - if (["glob", "grep"].includes(normalized)) { - return "search"; - } - - if (normalized === "bash") { - return "execute"; - } - - if (["webfetch", "websearch"].includes(normalized)) { - return "fetch"; - } - - if (["agent", "task", "sendmessage"].includes(normalized)) { - return "agent"; - } - - return "other"; -} - -export function toUsage(message: Extract): TokenUsage | undefined { - const raw = isRecord(message.usage) ? message.usage : null; - const cachedInput = toTokenCount(raw?.["cache_read_input_tokens"]); - const input = toTokenCount(raw?.["input_tokens"]); - const output = toTokenCount(raw?.["output_tokens"]); - const total = sumTokenCounts(input, output); - const cost = toCostAmount(message.total_cost_usd); - const usage = { - ...(cachedInput === null ? {} : { cachedInput }), - ...(cost === null ? {} : { cost: { amount: cost, currency: "USD" } }), - ...(input === null ? {} : { input }), - ...(output === null ? {} : { output }), - ...(total === null ? {} : { total }), - } satisfies TokenUsage; - - return Object.keys(usage).length === 0 ? undefined : usage; -} - -export function isLimit(message: Extract): boolean { - return ( - message.subtype === "error_max_turns" || - message.subtype === "error_max_budget_usd" || - message.subtype === "error_max_structured_output_retries" || - message.stop_reason === "max_tokens" || - message.terminal_reason === "max_turns" || - message.terminal_reason === "budget_exhausted" || - message.terminal_reason === "structured_output_retry_exhausted" - ); -} - -export function finishReason( - message: Extract, -): "limit" | "other" | "refusal" | "success" { - if (message.stop_reason === "refusal") { - return "refusal"; - } - - if (isLimit(message)) { - return "limit"; - } - - return message.terminal_reason === "background_requested" || - message.terminal_reason === "tool_deferred" || - message.terminal_reason === "tool_deferred_unavailable" - ? "other" - : "success"; -} - -export function isRetryable( - message: Exclude, { subtype: "success" }>, -): boolean { - return ( - message.terminal_reason === "api_error" || - message.terminal_reason === "model_error" || - message.terminal_reason === "blocking_limit" || - message.terminal_reason === "rapid_refill_breaker" - ); -} diff --git a/src/runtimes/claude/contract-permissions.ts b/src/runtimes/claude/contract-permissions.ts deleted file mode 100644 index 2b2f033..0000000 --- a/src/runtimes/claude/contract-permissions.ts +++ /dev/null @@ -1,526 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import type { CanUseTool, PermissionResult } from "@anthropic-ai/claude-agent-sdk"; - -import { - AuthorityOutcomeUnknownError, - itemSchema, - permissionInteractionSchema, -} from "../../contract"; -import type { - InteractionResolution, - PermissionInteraction, - PermissionOption, -} from "../../contract"; -import { createProviderMeta, ContractProjection, nonEmpty } from "../contract-projection"; -import { isRecord } from "./agent-sdk-json"; -import type { JsonObject } from "./agent-sdk-json"; -import { toolCategory } from "./contract-items"; -import type { ClaudeContractTranscript } from "./contract-transcript"; - -const { cause: providerCause, provenance } = createProviderMeta("anthropic"); - -export type ClaudePermissionOptions = Parameters[2]; -type PermissionOptions = ClaudePermissionOptions; - -interface PermissionCancellation { - onAbort: () => void; - readonly signals: Map void>; -} - -interface PendingPermission { - aborted: PermissionInteraction | null; - abortTask: Promise | null; - readonly bytes: number; - readonly cancellation: PermissionCancellation; - readonly interaction: PermissionInteraction; - readonly request: { - readonly input: Record; - readonly options: Record; - readonly toolName: string; - }; - readonly requestId: string; - readonly runId: string; - readonly sessionSuggestions: ClaudePermissionOptions["suggestions"]; - readonly toolUseId: string; -} - -interface OpeningPermission { - readonly bytes: number; - readonly cancellation: PermissionCancellation; - readonly request: PendingPermission["request"]; - readonly runId: string; - readonly task: Promise; - readonly toolUseId: string; -} - -function permissionOptions(options: ClaudePermissionOptions): PermissionOption[] { - const allowSession = options.suggestions?.some( - (suggestion) => suggestion.destination === "session", - ); - const result: PermissionOption[] = [ - { effect: "allow", id: "allow_once", label: "Allow once", scope: "once" }, - ]; - - if (allowSession) { - result.push({ - effect: "allow", - id: "allow_session", - label: "Allow for session", - scope: "session", - }); - } - - result.push({ effect: "deny", id: "deny_once", label: "Deny", scope: "once" }); - return result; -} - -export interface ClaudeContractPermissionsOptions { - readonly createId: () => string; - readonly interactionTimeoutMs: number; - readonly isRunFinishing: (runId: string) => boolean; - readonly maxPendingPermissionBytes: number; - readonly projection: ContractProjection; - readonly transcript: ClaudeContractTranscript; -} - -export class ClaudeContractPermissions { - readonly #createId: () => string; - #disposed = false; - readonly #interactionTimeoutMs: number; - readonly #isRunFinishing: (runId: string) => boolean; - readonly #maxPendingPermissionBytes: number; - readonly #openingPermissions = new Map(); - readonly #pendingPermissions = new Map(); - #pendingPermissionBytes = 0; - readonly #projection: ContractProjection; - readonly #textEncoder = new TextEncoder(); - readonly #transcript: ClaudeContractTranscript; - - constructor(options: ClaudeContractPermissionsOptions) { - this.#createId = options.createId; - this.#interactionTimeoutMs = options.interactionTimeoutMs; - this.#isRunFinishing = options.isRunFinishing; - this.#maxPendingPermissionBytes = options.maxPendingPermissionBytes; - this.#projection = options.projection; - this.#transcript = options.transcript; - } - - async openPermission( - runId: string, - toolName: string, - input: Record, - options: PermissionOptions, - ): Promise { - this.#assertActive(); - if (this.#isRunFinishing(runId)) { - throw new Error("Claude permission request outlived its active Run."); - } - const projectedInput = this.#transcript.toolInput(input); - - if (!isRecord(projectedInput)) { - throw new Error("Claude permission tool input must be finite JSON."); - } - - const request = { - input: projectedInput, - options: structuredClone( - Object.fromEntries(Object.entries(options).filter(([name]) => name !== "signal")), - ), - toolName, - }; - const opening = this.#openingPermissions.get(options.requestId); - - if (opening !== undefined) { - if ( - opening.runId !== runId || - opening.toolUseId !== options.toolUseID || - !isDeepStrictEqual(opening.request, request) - ) { - throw new Error( - `Claude permission request ${options.requestId} changed identity or content.`, - ); - } - - this.#trackSignal(opening.cancellation, options.signal); - return opening.task; - } - - const existingPermission = [...this.#pendingPermissions].find( - ([, pending]) => pending.requestId === options.requestId, - ); - - if (existingPermission !== undefined) { - const [interactionId, pending] = existingPermission; - - if ( - pending.runId !== runId || - pending.toolUseId !== options.toolUseID || - !isDeepStrictEqual(pending.request, request) - ) { - throw new Error( - `Claude permission request ${options.requestId} changed identity or content.`, - ); - } - - this.#trackSignal(pending.cancellation, options.signal); - const aborted = this.#abortedSignal(pending.cancellation); - if (aborted !== null) { - await this.#abortPermission(interactionId); - aborted.throwIfAborted(); - } - return interactionId; - } - - options.signal.throwIfAborted(); - const bytes = this.#textEncoder.encode(JSON.stringify(request)).byteLength; - - if (bytes > this.#maxPendingPermissionBytes - this.#pendingPermissionBytes) { - throw new RangeError("Claude pending permission budget is exhausted."); - } - - this.#pendingPermissionBytes += bytes; - const cancellation: PermissionCancellation = { - onAbort: () => {}, - signals: new Map(), - }; - this.#trackSignal(cancellation, options.signal); - const task = this.#createPermission( - runId, - toolName, - projectedInput, - options, - request, - bytes, - cancellation, - ); - const openingPermission: OpeningPermission = { - bytes, - cancellation, - request, - runId, - task, - toolUseId: options.toolUseID, - }; - this.#openingPermissions.set(options.requestId, openingPermission); - - try { - return await task; - } catch (error) { - this.#dropOpening(options.requestId, openingPermission); - throw error; - } - } - - async #createPermission( - runId: string, - toolName: string, - projectedInput: JsonObject, - options: PermissionOptions, - request: PendingPermission["request"], - bytes: number, - cancellation: PermissionCancellation, - ): Promise { - this.#throwIfAborted(cancellation); - const itemId = this.#transcript.id(runId, "tool", options.toolUseID); - const now = this.#projection.now(); - const current = this.#projection.item(runId, itemId); - const name = nonEmpty(toolName, "Tool"); - - if (current !== undefined && (current.kind !== "tool" || current.status !== "active")) { - throw new Error("Claude permission request references a terminal or non-tool item."); - } - - if (current === undefined) { - const item = itemSchema.parse({ - audience: "participants", - category: toolCategory(name), - createdAt: now.toISOString(), - id: itemId, - input: projectedInput, - kind: "tool", - name, - origin: name.startsWith("mcp__") ? "mcp" : "provider", - provenance: provenance("permission/requested", { - requestId: options.requestId, - toolUseId: options.toolUseID, - }), - runId, - status: "active", - title: nonEmpty(options.displayName ?? options.title, name), - updatedAt: now.toISOString(), - }); - await this.#retryUnknown(() => - this.#projection.putItem( - runId, - "permission/requested.tool", - providerCause("permission/requested", options.requestId), - item, - ), - ); - this.#transcript.markAuthoritativeToolInput(runId, itemId); - } - - this.#throwIfAborted(cancellation); - - const interactionId = this.#createId(); - const interaction = permissionInteractionSchema.parse({ - audience: "participants", - blocking: true, - createdAt: now.toISOString(), - expiresAt: new Date(now.getTime() + this.#interactionTimeoutMs).toISOString(), - id: interactionId, - itemId, - kind: "permission", - provenance: provenance("permission/requested", { - requestId: options.requestId, - toolUseId: options.toolUseID, - }), - request: { - ...(options.description === undefined ? {} : { description: options.description }), - options: permissionOptions(options), - subject: { itemId, type: "item" }, - title: nonEmpty(options.title ?? options.displayName, `Allow ${name}?`), - }, - runId, - status: "open", - }); - await this.#retryUnknown(() => - this.#projection.putInteraction( - runId, - "permission/requested", - providerCause("permission/requested", options.requestId), - interaction, - ), - ); - - this.#assertActive(); - if (this.#isRunFinishing(runId) || this.#projection.run(runId)?.status !== "active") { - throw new Error("Claude permission request outlived its active Run."); - } - - this.#openingPermissions.delete(options.requestId); - const pending: PendingPermission = { - aborted: null, - abortTask: null, - bytes, - cancellation, - interaction, - request, - requestId: options.requestId, - runId, - sessionSuggestions: options.suggestions - ?.filter((suggestion) => suggestion.destination === "session") - .map((suggestion) => structuredClone(suggestion)), - toolUseId: options.toolUseID, - }; - this.#pendingPermissions.set(interactionId, pending); - cancellation.onAbort = () => { - void this.#abortPermission(interactionId).catch(() => {}); - }; - const aborted = this.#abortedSignal(cancellation); - if (aborted !== null) { - await this.#abortPermission(interactionId); - aborted.throwIfAborted(); - } - return interactionId; - } - - async resolveInteraction( - interactionId: string, - resolution: InteractionResolution, - ): Promise { - this.#assertActive(); - const pending = this.#pendingPermissions.get(interactionId); - - if (pending === undefined) { - return null; - } - - if (this.#abortedSignal(pending.cancellation) !== null) { - await this.#abortPermission(interactionId); - return null; - } - - if (resolution.kind !== "permission") { - throw new Error("Claude permission interaction requires a permission resolution."); - } - - const selected = resolution.value.type === "selected" ? resolution.value.optionId : null; - const available = pending.interaction.request.options.some((option) => option.id === selected); - - if (selected !== null && !available) { - throw new Error("Claude permission resolution selected an unavailable option."); - } - - this.#projection.releaseInteraction(interactionId); - this.#dropPermission(interactionId); - - if (selected === "allow_once" || selected === "allow_session") { - return { - behavior: "allow", - toolUseID: pending.toolUseId, - updatedInput: pending.request.input, - ...(selected === "allow_session" && pending.sessionSuggestions !== undefined - ? { updatedPermissions: pending.sessionSuggestions } - : {}), - }; - } - - return { - behavior: "deny", - interrupt: resolution.value.type === "cancelled", - message: "Rejected by user.", - toolUseID: pending.toolUseId, - }; - } - - #abortedInteraction(interaction: PermissionInteraction): PermissionInteraction { - return permissionInteractionSchema.parse({ - ...interaction, - endedAt: this.#projection.now().toISOString(), - resolution: { type: "cancelled" }, - status: "resolved", - }); - } - - async #putAbortedInteraction( - interaction: PermissionInteraction, - requestId: string, - ): Promise { - await this.#retryUnknown(() => - this.#projection.putInteraction( - interaction.runId, - "permission/aborted", - providerCause("permission/aborted", requestId), - interaction, - ), - ); - this.#projection.releaseInteraction(interaction.id); - } - - #abortPermission(interactionId: string): Promise { - const pending = this.#pendingPermissions.get(interactionId); - - if (pending === undefined) { - return Promise.resolve(); - } - - if (pending.abortTask !== null) { - return pending.abortTask.catch(() => this.#abortPermission(interactionId)); - } - - if (this.#projection.run(pending.runId)?.status !== "active") { - this.#dropPermission(interactionId); - return Promise.resolve(); - } - - pending.aborted ??= this.#abortedInteraction(pending.interaction); - const task = this.#putAbortedInteraction(pending.aborted, pending.requestId) - .then(() => this.#dropPermission(interactionId)) - .finally(() => { - if (pending.abortTask === task) { - pending.abortTask = null; - } - }); - pending.abortTask = task; - return task; - } - - async #retryUnknown(operation: () => Promise): Promise { - try { - return await operation(); - } catch (error) { - if (!(error instanceof AuthorityOutcomeUnknownError)) { - throw error; - } - - return operation(); - } - } - - #trackSignal(cancellation: PermissionCancellation, signal: AbortSignal): void { - if (cancellation.signals.has(signal)) { - return; - } - - const onAbort = () => cancellation.onAbort(); - cancellation.signals.set(signal, onAbort); - signal.addEventListener("abort", onAbort, { once: true }); - if (signal.aborted) { - onAbort(); - } - } - - #abortedSignal(cancellation: PermissionCancellation): AbortSignal | null { - for (const signal of cancellation.signals.keys()) { - if (signal.aborted) { - return signal; - } - } - - return null; - } - - #throwIfAborted(cancellation: PermissionCancellation): void { - this.#abortedSignal(cancellation)?.throwIfAborted(); - } - - #clearSignals(cancellation: PermissionCancellation): void { - cancellation.onAbort = () => {}; - for (const [signal, onAbort] of cancellation.signals) { - signal.removeEventListener("abort", onAbort); - } - cancellation.signals.clear(); - } - - #dropOpening(requestId: string, opening: OpeningPermission): void { - if (this.#openingPermissions.get(requestId) !== opening) { - return; - } - - this.#openingPermissions.delete(requestId); - this.#pendingPermissionBytes -= opening.bytes; - this.#clearSignals(opening.cancellation); - } - - #dropPermission(interactionId: string): void { - const pending = this.#pendingPermissions.get(interactionId); - - if (pending !== undefined) { - this.#pendingPermissionBytes -= pending.bytes; - this.#pendingPermissions.delete(interactionId); - this.#clearSignals(pending.cancellation); - } - } - - releaseRun(runId: string): void { - for (const [requestId, opening] of this.#openingPermissions) { - if (opening.runId === runId) { - this.#dropOpening(requestId, opening); - } - } - for (const [id, pending] of this.#pendingPermissions) { - if (pending.runId === runId) { - this.#dropPermission(id); - } - } - } - - dispose(): void { - this.#disposed = true; - for (const [requestId, opening] of this.#openingPermissions) { - this.#dropOpening(requestId, opening); - } - for (const id of this.#pendingPermissions.keys()) { - this.#dropPermission(id); - } - this.#pendingPermissionBytes = 0; - } - - #assertActive(): void { - if (this.#disposed) { - throw new Error("Claude Contract permissions are disposed."); - } - } -} diff --git a/src/runtimes/claude/contract-result.ts b/src/runtimes/claude/contract-result.ts deleted file mode 100644 index c9552ff..0000000 --- a/src/runtimes/claude/contract-result.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; - -import { itemSchema } from "../../contract"; -import type { ProtocolError } from "../../contract"; -import { asJsonValue, type ContractProjection } from "../contract-projection"; -import { createProviderMeta } from "../contract-adapter-meta"; -import { finishReason, isLimit, isRetryable, toUsage } from "./contract-items"; - -const { cause: providerCause, provenance } = createProviderMeta("anthropic"); - -export interface FinishClaudeResultOptions { - readonly id: (runId: string, kind: string, nativeId: string) => string; - readonly message: Extract; - readonly onRunReleased: (runId: string) => void; - readonly projection: ContractProjection; - readonly runId: string; -} - -export async function finishClaudeResult(options: FinishClaudeResultOptions): Promise { - const { id, message, onRunReleased, projection, runId } = options; - const event = `result/${message.subtype}`; - const cause = providerCause(event, message.uuid); - const usage = toUsage(message); - if (usage !== undefined) { - await projection.updateUsage(runId, event, cause, usage); - } - - if (message.subtype === "success") { - const hasMessageText = projection - .items(runId) - .some( - (item) => - item.kind === "message" && - item.status === "completed" && - item.content.flatMap((block) => (block.type === "text" ? [block.text] : [])).join("") === - message.result, - ); - if (message.result.length > 0 && !hasMessageText) { - const now = projection.now().toISOString(); - await projection.putItem( - runId, - `${event}/final`, - cause, - itemSchema.parse({ - audience: "participants", - content: [{ text: message.result, type: "text" }], - createdAt: now, - endedAt: now, - id: id(runId, "result", `${message.uuid}:final`), - kind: "message", - phase: "final", - provenance: provenance(event, { messageId: message.uuid }), - role: "agent", - runId, - status: "completed", - updatedAt: now, - }), - ); - } - - if (message.structured_output !== undefined) { - const now = projection.now().toISOString(); - const structured = asJsonValue(message.structured_output); - if (structured !== undefined) { - await projection.putItem( - runId, - `${event}/structured_output`, - cause, - itemSchema.parse({ - audience: "participants", - content: [{ type: "json", value: structured }], - createdAt: now, - endedAt: now, - id: id(runId, "structured", message.uuid), - kind: "artifact", - name: "structured-output.json", - provenance: provenance(event, { messageId: message.uuid }), - runId, - status: "completed", - updatedAt: now, - }), - ); - } - } - - await projection.finishRun({ - activeItemStatus: "cancelled", - cause, - event, - finishReason: finishReason(message), - runId, - status: "completed", - }); - onRunReleased(runId); - return; - } - - const cancelled = - message.terminal_reason === "aborted_streaming" || message.terminal_reason === "aborted_tools"; - - if (isLimit(message)) { - await projection.finishRun({ - activeItemStatus: "cancelled", - cause, - event, - finishReason: "limit", - runId, - status: "completed", - }); - onRunReleased(runId); - return; - } - - const error = { - code: `anthropic.${message.subtype}`, - ...(message.terminal_reason === undefined - ? {} - : { details: { terminalReason: message.terminal_reason } }), - message: message.errors.join("\n") || "Agent SDK run failed.", - retryable: isRetryable(message), - } satisfies ProtocolError; - await projection.finishRun({ - cause, - ...(cancelled ? {} : { error }), - event, - runId, - status: cancelled ? "cancelled" : "failed", - }); - onRunReleased(runId); -} diff --git a/src/runtimes/claude/contract-transcript-state.ts b/src/runtimes/claude/contract-transcript-state.ts deleted file mode 100644 index ddca1a2..0000000 --- a/src/runtimes/claude/contract-transcript-state.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { asJsonValue } from "../contract-projection"; - -export interface ToolInputBuffer { - readonly bytes: number; - readonly overflowed: boolean; - readonly text: string; -} - -export class ClaudeContractTranscriptState { - readonly #authoritativeToolInputs = new Set(); - readonly #blockToolIds = new Map(); - readonly #createId: () => string; - readonly #ids = new Map>(); - readonly #maxToolInputBytes: number; - readonly #textEncoder = new TextEncoder(); - #toolInputBytes = 0; - readonly #toolInputFragments = new Map(); - - constructor(createId: () => string, maxToolInputBytes: number) { - this.#createId = createId; - this.#maxToolInputBytes = maxToolInputBytes; - } - - id(runId: string, kind: string, nativeId: string): string { - const candidate = nativeId.length > 0 ? `${kind}:${nativeId}` : ""; - - if (candidate.length > 0 && candidate.length <= 256) { - return candidate; - } - - let ids = this.#ids.get(runId); - - if (ids === undefined) { - ids = new Map(); - this.#ids.set(runId, ids); - } - - const key = `${kind}:${nativeId}`; - let id = ids.get(key); - - if (id === undefined) { - id = this.#createId(); - ids.set(key, id); - } - - return id; - } - - reasoningId(runId: string, messageId: string): string { - return this.id(runId, "reasoning", `${messageId}:reasoning`); - } - - hasAuthoritativeToolInput(key: string): boolean { - return this.#authoritativeToolInputs.has(key); - } - - markAuthoritativeToolInput(runId: string, itemId: string): void { - this.#authoritativeToolInputs.add(`${runId}:${itemId}`); - } - - setBlockToolId(key: string, toolId: string): void { - this.#blockToolIds.set(key, toolId); - } - - blockToolId(key: string): string | undefined { - return this.#blockToolIds.get(key); - } - - deleteBlockToolId(key: string): void { - this.#blockToolIds.delete(key); - } - - deleteBlockToolIdsForTool(toolId: string): void { - for (const [key, value] of this.#blockToolIds) { - if (value === toolId) { - this.#blockToolIds.delete(key); - } - } - } - - appendToolInput(key: string, fragment: string): void { - const current = this.#toolInputFragments.get(key) ?? { - bytes: 0, - overflowed: false, - text: "", - }; - - if (current.overflowed) { - return; - } - - const addedBytes = this.#textEncoder.encode(fragment).byteLength; - - if (addedBytes > this.#maxToolInputBytes - this.#toolInputBytes) { - this.#toolInputBytes -= current.bytes; - this.#toolInputFragments.set(key, { bytes: 0, overflowed: true, text: "" }); - return; - } - - this.#toolInputBytes += addedBytes; - this.#toolInputFragments.set(key, { - bytes: current.bytes + addedBytes, - overflowed: false, - text: current.text + fragment, - }); - } - - toolInputBuffer(key: string): ToolInputBuffer | undefined { - return this.#toolInputFragments.get(key); - } - - dropToolInput(key: string): void { - const buffer = this.#toolInputFragments.get(key); - - if (buffer !== undefined) { - this.#toolInputBytes -= buffer.bytes; - this.#toolInputFragments.delete(key); - } - } - - toolInput(value: unknown) { - const input = asJsonValue(value); - - if ( - input !== undefined && - this.#textEncoder.encode(JSON.stringify(input)).byteLength > this.#maxToolInputBytes - ) { - throw new RangeError("Claude tool input exceeds its byte limit."); - } - - return input; - } - - releaseRun(runId: string): void { - this.#ids.delete(runId); - - for (const key of this.#authoritativeToolInputs) { - if (key.startsWith(`${runId}:`)) { - this.#authoritativeToolInputs.delete(key); - } - } - for (const key of this.#blockToolIds.keys()) { - if (key.startsWith(`${runId}:`)) { - this.#blockToolIds.delete(key); - } - } - for (const key of this.#toolInputFragments.keys()) { - if (key.startsWith(`${runId}:`)) { - this.dropToolInput(key); - } - } - } - - dispose(): void { - this.#authoritativeToolInputs.clear(); - this.#blockToolIds.clear(); - this.#ids.clear(); - this.#toolInputBytes = 0; - this.#toolInputFragments.clear(); - } -} diff --git a/src/runtimes/claude/contract-transcript.ts b/src/runtimes/claude/contract-transcript.ts deleted file mode 100644 index d043aa5..0000000 --- a/src/runtimes/claude/contract-transcript.ts +++ /dev/null @@ -1,763 +0,0 @@ -import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; - -import { itemSchema } from "../../contract"; -import type { ContentBlock } from "../../contract"; -import { asJsonValue, ContractProjection, nonEmpty } from "../contract-projection"; -import { isRecord, readNumber, readRecord, readString } from "./agent-sdk-json"; -import type { JsonObject } from "./agent-sdk-json"; -import { toContentBlocks, toolCategory } from "./contract-items"; -import { createProviderMeta } from "../contract-adapter-meta"; -import { ClaudeContractTranscriptState } from "./contract-transcript-state"; -import { finishClaudeResult } from "./contract-result"; - -const { cause: providerCause, provenance } = createProviderMeta("anthropic"); - -export interface ClaudeContractTranscriptOptions { - readonly createId: () => string; - readonly maxToolInputBytes: number; - readonly onRunReleased: (runId: string) => void; - readonly projection: ContractProjection; -} - -export class ClaudeContractTranscript { - readonly #createId: () => string; - readonly #onRunReleased: (runId: string) => void; - readonly #projection: ContractProjection; - readonly #state: ClaudeContractTranscriptState; - - constructor(options: ClaudeContractTranscriptOptions) { - this.#createId = options.createId; - this.#onRunReleased = options.onRunReleased; - this.#projection = options.projection; - this.#state = new ClaudeContractTranscriptState(options.createId, options.maxToolInputBytes); - } - - async handleMessage(message: SDKMessage, runId: string): Promise { - switch (message.type) { - case "assistant": - await this.#onAssistant(message, runId); - return false; - case "result": - await this.#onResult(message, runId); - return true; - case "stream_event": - await this.#onStreamEvent(message, runId); - return false; - case "tool_progress": - await this.#onToolProgress(message, runId); - return false; - case "user": - await this.#onUserMessage(message, runId); - return false; - case "system": - await this.#onSystemMessage(message, runId); - return false; - default: - return false; - } - } - - async #onAssistant( - message: Extract, - runId: string, - ): Promise { - const event = "assistant/message"; - const occurredAt = this.#projection.now().toISOString(); - const messageId = this.id(runId, "message", message.uuid); - const reasoningId = this.#reasoningId(runId, messageId); - const text: ContentBlock[] = []; - const reasoning: ContentBlock[] = []; - - for (const block of message.message.content) { - if (!isRecord(block)) { - continue; - } - - if (block["type"] === "text") { - const content = toContentBlocks(block); - - if (content.length > 0) { - await this.#ensureMessage(runId, messageId, event, message.uuid); - text.push(...content); - } - continue; - } - - if (block["type"] === "thinking") { - const thinking = readString(block, "thinking"); - if (thinking !== null) { - await this.#ensureReasoning(runId, messageId, event, message.uuid); - reasoning.push({ text: thinking, type: "text" }); - } - continue; - } - - if (this.#isToolUse(block)) { - await this.#putToolUse(runId, messageId, block, event, occurredAt); - continue; - } - - const toolUseId = readString(block, "tool_use_id"); - if (toolUseId !== null) { - await this.#completeTool(runId, toolUseId, block, undefined, event, occurredAt); - } - } - - if (text.length > 0 || this.#projection.item(runId, messageId)?.kind === "message") { - const current = this.#projection.item(runId, messageId); - if (current?.status === "active" || current === undefined) { - await this.#projection.putItem( - runId, - event, - providerCause(event, message.uuid), - itemSchema.parse({ - audience: "participants", - content: text, - createdAt: current?.createdAt ?? occurredAt, - endedAt: occurredAt, - ...(message.error === undefined - ? { status: "completed" } - : { - error: { - code: `anthropic.${message.error}`, - message: `Assistant message failed: ${message.error}.`, - retryable: - message.error === "overloaded" || - message.error === "rate_limit" || - message.error === "server_error", - }, - status: "failed", - }), - id: messageId, - kind: "message", - phase: "final", - provenance: provenance(event, { - messageId: message.uuid, - ...(message.parent_tool_use_id === null - ? {} - : { parentToolUseId: message.parent_tool_use_id }), - }), - role: "agent", - runId, - updatedAt: occurredAt, - ...(message.supersedes === undefined && message.timestamp === undefined - ? {} - : { - extensions: { - ...(message.timestamp === undefined - ? {} - : { "anthropic.agent-sdk/source-timestamp": message.timestamp }), - ...(message.supersedes === undefined - ? {} - : { "anthropic.agent-sdk/supersedes": message.supersedes }), - }, - }), - }), - ); - } - } - - const currentReasoning = this.#projection.item(runId, reasoningId); - if (reasoning.length > 0 || currentReasoning?.kind === "reasoning") { - if (currentReasoning?.status === "active" || currentReasoning === undefined) { - await this.#projection.putItem( - runId, - event, - providerCause(event, message.uuid), - itemSchema.parse({ - audience: "participants", - content: reasoning, - createdAt: currentReasoning?.createdAt ?? occurredAt, - endedAt: occurredAt, - id: reasoningId, - kind: "reasoning", - provenance: provenance(event, { messageId: message.uuid }), - runId, - status: "completed", - updatedAt: occurredAt, - ...(message.timestamp === undefined - ? {} - : { - extensions: { - "anthropic.agent-sdk/source-timestamp": message.timestamp, - }, - }), - }), - ); - } - } - } - - async #onStreamEvent( - message: Extract, - runId: string, - ): Promise { - const event = isRecord(message.event) ? message.event : null; - const eventType = readString(event, "type"); - const messageId = this.id(runId, "message", message.uuid); - - if (eventType === "message_start") { - return; - } - - if (eventType === "content_block_start") { - const block = readRecord(event, "content_block"); - const index = readNumber(event, "index"); - - if (block?.["type"] === "text") { - await this.#ensureMessage(runId, messageId, "stream/content_block_start", message.uuid); - const text = readString(block, "text"); - if (text !== null) { - await this.#appendText( - runId, - messageId, - "message.text", - text, - "stream/content_block_start", - ); - } - } else if (block?.["type"] === "thinking") { - const reasoningId = await this.#ensureReasoning( - runId, - messageId, - "stream/content_block_start", - message.uuid, - ); - const text = readString(block, "thinking"); - if (text !== null) { - await this.#appendText( - runId, - reasoningId, - "reasoning.text", - text, - "stream/content_block_start", - ); - } - } else if (block !== null && this.#isToolUse(block)) { - const toolId = await this.#putToolUse( - runId, - messageId, - block, - "stream/content_block_start", - this.#projection.now().toISOString(), - ); - if (index !== null && !this.#state.hasAuthoritativeToolInput(`${runId}:${toolId}`)) { - this.#state.setBlockToolId(`${runId}:${message.uuid}:${index}`, toolId); - } - } - return; - } - - if (eventType === "content_block_delta") { - const delta = readRecord(event, "delta"); - const deltaType = readString(delta, "type"); - - if (deltaType === "text_delta") { - await this.#ensureMessage(runId, messageId, "stream/text_delta", message.uuid); - await this.#appendText( - runId, - messageId, - "message.text", - readString(delta, "text") ?? "", - "stream/text_delta", - ); - } else if (deltaType === "thinking_delta") { - const reasoningId = await this.#ensureReasoning( - runId, - messageId, - "stream/thinking_delta", - message.uuid, - ); - await this.#appendText( - runId, - reasoningId, - "reasoning.text", - readString(delta, "thinking") ?? "", - "stream/thinking_delta", - ); - } else if (deltaType === "input_json_delta") { - const index = readNumber(event, "index"); - const toolId = - index === null ? undefined : this.#state.blockToolId(`${runId}:${message.uuid}:${index}`); - const fragment = readString(delta, "partial_json"); - if (toolId !== undefined && fragment !== null) { - this.#state.appendToolInput(`${runId}:${toolId}`, fragment); - } - } - return; - } - - if (eventType === "content_block_stop") { - const index = readNumber(event, "index"); - const key = index === null ? null : `${runId}:${message.uuid}:${index}`; - const toolId = key === null ? undefined : this.#state.blockToolId(key); - - if (toolId !== undefined) { - const item = this.#projection.item(runId, toolId); - const fragmentKey = `${runId}:${toolId}`; - const buffer = this.#state.toolInputBuffer(fragmentKey); - if ( - item?.kind === "tool" && - item.status === "active" && - buffer !== undefined && - !buffer.overflowed && - !this.#state.hasAuthoritativeToolInput(fragmentKey) - ) { - try { - const input = this.toolInput(JSON.parse(buffer.text)); - if (input !== undefined) { - await this.#projection.putItem( - runId, - "stream/input_json", - providerCause("stream/input_json", toolId), - itemSchema.parse({ - ...item, - input, - updatedAt: this.#projection.now().toISOString(), - }), - ); - } - } catch {} - } - this.#state.dropToolInput(fragmentKey); - } - if (key !== null) { - this.#state.deleteBlockToolId(key); - } - return; - } - } - - async #onUserMessage( - message: Extract, - runId: string, - ): Promise { - const blocks = Array.isArray(message.message.content) ? message.message.content : []; - const occurredAt = this.#projection.now().toISOString(); - - for (const block of blocks) { - if (!isRecord(block) || block["type"] !== "tool_result") { - continue; - } - - const toolUseId = readString(block, "tool_use_id"); - if (toolUseId !== null) { - await this.#completeTool( - runId, - toolUseId, - block, - message.tool_use_result, - "user/tool_result", - occurredAt, - ); - } - } - } - - async #onToolProgress( - message: Extract, - runId: string, - ): Promise { - const itemId = this.id(runId, "tool", message.tool_use_id); - const name = nonEmpty(message.tool_name, "Tool"); - let item = this.#projection.item(runId, itemId); - - if (item === undefined) { - const now = this.#projection.now().toISOString(); - item = await this.#projection.putItem( - runId, - "tool/progress", - providerCause("tool/progress", message.tool_use_id), - itemSchema.parse({ - audience: "participants", - category: toolCategory(name), - createdAt: now, - id: itemId, - kind: "tool", - name, - origin: name.startsWith("mcp__") ? "mcp" : "provider", - provenance: provenance("tool/progress", { toolUseId: message.tool_use_id }), - runId, - status: "active", - updatedAt: now, - }), - ); - } - - if (item.status === "active") { - await this.#projection.replacePreview({ - channel: "tool.progress", - itemId, - runId, - text: `${name} (${message.elapsed_time_seconds.toFixed(1)}s)`, - }); - } - } - - async #onSystemMessage( - message: Extract, - runId: string, - ): Promise { - if (message.subtype === "files_persisted" && message.files.length > 0) { - const changes = message.files.flatMap((file) => - file.filename.trim().length === 0 ? [] : [{ operation: "update", path: file.filename }], - ); - - if (changes.length === 0) { - return; - } - - const now = this.#projection.now().toISOString(); - const id = this.id(runId, "files", message.uuid); - - if (this.#projection.item(runId, id) !== undefined) { - return; - } - - await this.#projection.putItem( - runId, - "system/files_persisted", - providerCause("system/files_persisted", message.uuid), - itemSchema.parse({ - audience: "participants", - changes, - createdAt: now, - endedAt: now, - id, - kind: "change", - provenance: provenance("system/files_persisted", { messageId: message.uuid }), - runId, - status: "completed", - updatedAt: now, - }), - ); - return; - } - - if (message.subtype === "task_started") { - const now = this.#projection.now().toISOString(); - const id = this.id(runId, "task", message.task_id); - - if (this.#projection.item(runId, id) !== undefined) { - return; - } - - const name = nonEmpty( - message.subagent_type ?? message.workflow_name ?? message.task_type, - "Agent", - ); - await this.#projection.putItem( - runId, - "system/task_started", - providerCause("system/task_started", message.task_id), - itemSchema.parse({ - audience: message.skip_transcript === true ? "operators" : "participants", - category: "agent", - createdAt: now, - id, - input: message.prompt, - kind: "tool", - name, - origin: "provider", - provenance: provenance("system/task_started", { taskId: message.task_id }), - runId, - status: "active", - ...(message.description.trim().length === 0 ? {} : { title: message.description }), - updatedAt: now, - }), - ); - return; - } - - if (message.subtype === "task_progress") { - const id = this.id(runId, "task", message.task_id); - const item = this.#projection.item(runId, id); - if (item?.kind === "tool" && item.status === "active") { - await this.#projection.replacePreview({ - channel: "tool.progress", - itemId: id, - runId, - text: message.summary ?? message.description, - }); - } - return; - } - - if (message.subtype === "task_updated") { - const id = this.id(runId, "task", message.task_id); - const item = this.#projection.item(runId, id); - const status = message.patch.status; - if (item?.kind === "tool" && item.status === "active" && status !== undefined) { - await this.#projection.replacePreview({ - channel: "tool.progress", - itemId: id, - runId, - text: nonEmpty(message.patch.description, `Agent task ${status}`), - }); - } - return; - } - - if (message.subtype === "task_notification") { - const id = this.id(runId, "task", message.task_id); - const item = this.#projection.item(runId, id); - if (item?.kind === "tool" && item.status === "active") { - const now = this.#projection.now().toISOString(); - const failed = message.status === "failed"; - await this.#projection.putItem( - runId, - "system/task_notification", - providerCause("system/task_notification", message.task_id), - itemSchema.parse({ - ...item, - endedAt: now, - ...(failed - ? { - error: { - code: "anthropic.task_failed", - message: nonEmpty(message.summary, "Agent task failed."), - retryable: false, - }, - } - : {}), - output: toContentBlocks(message.summary), - status: failed ? "failed" : message.status === "stopped" ? "cancelled" : "completed", - structuredOutput: asJsonValue(message.usage), - updatedAt: now, - }), - ); - } - } - } - - async #onResult(message: Extract, runId: string): Promise { - await finishClaudeResult({ - id: (id, kind, nativeId) => this.id(id, kind, nativeId), - message, - onRunReleased: this.#onRunReleased, - projection: this.#projection, - runId, - }); - } - - async #ensureMessage( - runId: string, - messageId: string, - event: string, - nativeMessageId: string, - ): Promise { - if (this.#projection.item(runId, messageId) !== undefined) { - return; - } - - const now = this.#projection.now().toISOString(); - await this.#projection.putItem( - runId, - event, - providerCause(event, nativeMessageId), - itemSchema.parse({ - audience: "participants", - content: [], - createdAt: now, - id: messageId, - kind: "message", - phase: "final", - provenance: provenance(event, { messageId: nativeMessageId }), - role: "agent", - runId, - status: "active", - updatedAt: now, - }), - ); - } - - async #ensureReasoning( - runId: string, - messageId: string, - event: string, - nativeMessageId: string, - ): Promise { - const id = this.#reasoningId(runId, messageId); - - if (this.#projection.item(runId, id) === undefined) { - const now = this.#projection.now().toISOString(); - await this.#projection.putItem( - runId, - event, - providerCause(event, nativeMessageId), - itemSchema.parse({ - audience: "participants", - content: [], - createdAt: now, - id, - kind: "reasoning", - provenance: provenance(event, { messageId: nativeMessageId }), - runId, - status: "active", - updatedAt: now, - }), - ); - } - - return id; - } - - async #appendText( - runId: string, - itemId: string, - channel: "message.text" | "reasoning.text", - delta: string, - event: string, - ): Promise { - await this.#projection.appendText({ - cause: providerCause(event, itemId), - channel, - delta, - event, - itemId, - runId, - }); - } - - async #putToolUse( - runId: string, - parentMessageId: string, - block: JsonObject, - event: string, - occurredAt: string, - ): Promise { - const nativeId = readString(block, "id") ?? this.#createId(); - const id = this.id(runId, "tool", nativeId); - const existing = this.#projection.item(runId, id); - const inputKey = `${runId}:${id}`; - const authoritative = event === "assistant/message"; - - if (existing !== undefined && existing.status !== "active") { - return id; - } - - if (!authoritative && this.#state.hasAuthoritativeToolInput(inputKey)) { - return id; - } - - if (authoritative) { - this.#state.dropToolInput(inputKey); - this.#state.deleteBlockToolIdsForTool(id); - } - - const name = nonEmpty(readString(block, "name"), "Tool"); - const type = readString(block, "type"); - const server = readString(block, "server_name"); - const input = this.toolInput(block["input"]); - await this.#projection.putItem( - runId, - event, - providerCause(event, nativeId), - itemSchema.parse({ - audience: "participants", - category: toolCategory(name), - createdAt: existing?.createdAt ?? occurredAt, - id, - input, - kind: "tool", - name, - origin: type === "mcp_tool_use" || name.startsWith("mcp__") ? "mcp" : "provider", - provenance: provenance(event, { messageId: parentMessageId, toolUseId: nativeId }), - runId, - ...(server === null || server.trim().length === 0 ? {} : { server }), - status: "active", - title: name, - updatedAt: occurredAt, - }), - ); - if (authoritative) { - this.#state.markAuthoritativeToolInput(runId, id); - } - return id; - } - - async #completeTool( - runId: string, - nativeToolId: string, - block: JsonObject, - structuredOutput: unknown, - event: string, - occurredAt: string, - ): Promise { - const id = this.id(runId, "tool", nativeToolId); - const existing = this.#projection.item(runId, id); - - if (existing !== undefined && (existing.kind !== "tool" || existing.status !== "active")) { - return; - } - - const failed = block["is_error"] === true; - const output = toContentBlocks(block["content"]); - const structured = asJsonValue(structuredOutput); - const errorText = output - .flatMap((entry) => (entry.type === "text" ? [entry.text] : [])) - .join("\n"); - await this.#projection.putItem( - runId, - event, - providerCause(event, nativeToolId), - itemSchema.parse({ - audience: "participants", - category: existing?.kind === "tool" ? existing.category : "other", - createdAt: existing?.createdAt ?? occurredAt, - endedAt: occurredAt, - ...(failed - ? { - error: { - code: "anthropic.tool_failed", - message: errorText || "Tool failed.", - retryable: false, - }, - } - : {}), - id, - input: existing?.kind === "tool" ? existing.input : undefined, - kind: "tool", - name: existing?.kind === "tool" ? existing.name : "Tool", - origin: existing?.kind === "tool" ? existing.origin : "provider", - output, - provenance: provenance(event, { toolUseId: nativeToolId }), - runId, - status: failed ? "failed" : "completed", - ...(structured === undefined ? {} : { structuredOutput: structured }), - title: existing?.kind === "tool" ? existing.title : undefined, - updatedAt: occurredAt, - }), - ); - } - - #reasoningId(runId: string, messageId: string): string { - return this.#state.reasoningId(runId, messageId); - } - - id(runId: string, kind: string, nativeId: string): string { - return this.#state.id(runId, kind, nativeId); - } - - #isToolUse(block: JsonObject): boolean { - return ["mcp_tool_use", "server_tool_use", "tool_use"].includes( - readString(block, "type") ?? "", - ); - } - - markAuthoritativeToolInput(runId: string, itemId: string): void { - this.#state.markAuthoritativeToolInput(runId, itemId); - } - - toolInput(value: unknown) { - return this.#state.toolInput(value); - } - - releaseRun(runId: string): void { - this.#state.releaseRun(runId); - } - - dispose(): void { - this.#state.dispose(); - } -} diff --git a/src/runtimes/contract-adapter-meta.ts b/src/runtimes/contract-adapter-meta.ts deleted file mode 100644 index e11700f..0000000 --- a/src/runtimes/contract-adapter-meta.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { jsonValueSchema } from "../contract"; -import type { MutationCause } from "../contract"; - -export function asJsonValue(value: unknown) { - const parsed = jsonValueSchema.safeParse(value); - return parsed.success ? parsed.data : undefined; -} - -export function nonEmpty(value: string | null | undefined, fallback: string): string { - return value?.trim() || fallback; -} - -export function createProviderMeta(provider: string) { - return { - cause(event: string, id?: string): MutationCause { - return { - providerEventId: `${event}${id === undefined ? "" : `:${id}`}`.slice(0, 256), - type: "provider", - }; - }, - provenance(event: string, nativeIds?: Readonly>) { - const boundedIds = Object.fromEntries( - Object.entries(nativeIds ?? {}).filter( - ([, value]) => value.length > 0 && value.length <= 256, - ), - ); - - return { - event, - ...(Object.keys(boundedIds).length === 0 ? {} : { nativeIds: boundedIds }), - provider, - }; - }, - }; -} diff --git a/src/runtimes/contract-projection-authority.ts b/src/runtimes/contract-projection-authority.ts deleted file mode 100644 index 0349568..0000000 --- a/src/runtimes/contract-projection-authority.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import { - assertProtocolAdmission, - AuthorityOutcomeUnknownError, - authorityContent, -} from "../contract"; -import type { AuthorityOperation, MutationCause, ProtocolAdmissionLimits } from "../contract"; -import { createDriverId } from "../protocol/id"; - -export const MAX_PENDING_MUTATION_BYTES = 32 * 1_024 * 1_024; -export const MAX_PENDING_MUTATIONS = 1_024; - -export interface ContractAuthorityUpdate { - readonly cause: MutationCause; - readonly event: string; - readonly mutationId: string; - readonly operations: readonly AuthorityOperation[]; - readonly runId: string; - readonly sessionId: string; -} - -export interface AuthorityWrite { - readonly intent: unknown; - readonly key: string; - readonly mutationId: string; - readonly update: Omit; -} - -export interface UnknownAuthorityWrite extends AuthorityWrite { - readonly error: AuthorityOutcomeUnknownError; -} - -export interface QueuedMutation { - readonly bytes: number; - readonly reject: (reason: unknown) => void; - readonly run: () => Promise; -} - -export function authorityKey(runId: string, event: string, cause: MutationCause): string { - const causeId = - cause.type === "command" - ? cause.commandId - : cause.type === "provider" - ? cause.providerEventId - : cause.type === "alarm" - ? cause.alarm - : cause.name; - return JSON.stringify([runId, event, cause.type, causeId]); -} - -export interface ContractProjectionAuthorityOptions { - readonly active: () => boolean; - readonly admissionLimits?: ProtocolAdmissionLimits | undefined; - readonly apply: (operations: readonly AuthorityOperation[]) => void; - readonly authority: (update: ContractAuthorityUpdate) => Promise; - readonly sessionId: string; -} - -export class ContractProjectionAuthority { - readonly #active: () => boolean; - readonly #admissionLimits: ProtocolAdmissionLimits | undefined; - readonly #apply: (operations: readonly AuthorityOperation[]) => void; - readonly #authority: (update: ContractAuthorityUpdate) => Promise; - readonly #sessionId: string; - #unknown: UnknownAuthorityWrite | undefined; - - constructor(options: ContractProjectionAuthorityOptions) { - this.#active = options.active; - this.#admissionLimits = options.admissionLimits; - this.#apply = options.apply; - this.#authority = options.authority; - this.#sessionId = options.sessionId; - } - - get unknown(): UnknownAuthorityWrite | undefined { - return this.#unknown; - } - - clear(): void { - this.#unknown = undefined; - } - - assertRetry( - key: string, - intent: unknown, - unknownAtEnqueue: UnknownAuthorityWrite | undefined, - ): void { - const unknown = this.#unknown; - - if (unknown === undefined) { - return; - } - - if (unknownAtEnqueue !== unknown || unknown.key !== key) { - throw unknown.error; - } - - if (!isDeepStrictEqual(unknown.intent, intent)) { - throw new AuthorityOutcomeUnknownError( - "Authority retry changed while its outcome was unknown.", - ); - } - } - - async commit( - runId: string, - event: string, - cause: MutationCause, - operations: readonly AuthorityOperation[], - intent: unknown, - reuseDerived = false, - ): Promise { - if (operations.length === 0) { - return operations; - } - - const update = { cause, event, operations, runId, sessionId: this.#sessionId }; - const key = authorityKey(runId, event, cause); - let pending: AuthorityWrite; - - if (this.#unknown === undefined) { - pending = { - intent: structuredClone(intent), - key, - mutationId: createDriverId(), - update, - }; - } else if (this.#unknown.key !== key) { - throw this.#unknown.error; - } else if ( - !isDeepStrictEqual(this.#unknown.intent, intent) || - (!reuseDerived && !isDeepStrictEqual(this.#unknown.update, update)) - ) { - throw new AuthorityOutcomeUnknownError( - `Authority write ${event} changed while its outcome was unknown.`, - ); - } else { - pending = this.#unknown; - } - - const submission = structuredClone({ ...pending.update, mutationId: pending.mutationId }); - - if (this.#admissionLimits !== undefined) { - assertProtocolAdmission( - submission, - this.#admissionLimits, - authorityContent(submission.operations), - ); - } - - try { - await this.#authority(submission); - } catch (error) { - if (error instanceof AuthorityOutcomeUnknownError && this.#active()) { - this.#unknown = { ...pending, error }; - } else { - this.#unknown = undefined; - } - throw error; - } - - if (!this.#active()) { - return pending.update.operations; - } - - try { - this.#apply(pending.update.operations); - } catch (error) { - const unknown = new AuthorityOutcomeUnknownError( - `Authority write ${event} committed but local apply failed.`, - { cause: error }, - ); - this.#unknown = { ...pending, error: unknown }; - throw unknown; - } - - this.#unknown = undefined; - return pending.update.operations; - } -} diff --git a/src/runtimes/contract-projection-preview.ts b/src/runtimes/contract-projection-preview.ts deleted file mode 100644 index 1ad260d..0000000 --- a/src/runtimes/contract-projection-preview.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { compareTimestamps, itemSchema } from "../contract"; -import type { - ContentBlock, - Item, - MutationCause, - PreviewUpdate, - ProtocolAdmissionLimits, -} from "../contract"; -import type { ContractAuthorityUpdate } from "./contract-projection-authority"; - -export interface ContractPreviewUpdate { - readonly runId: string; - readonly sessionId: string; - readonly update: PreviewUpdate; -} - -export interface ContractProjectionOptions { - readonly admissionLimits?: ProtocolAdmissionLimits | undefined; - readonly authority: (update: ContractAuthorityUpdate) => Promise; - readonly now?: (() => Date) | undefined; - readonly preview: (update: ContractPreviewUpdate) => void; - readonly previewCheckpointBytes?: number | undefined; - readonly previewReplaceIntervalMs?: number | undefined; - readonly sessionId: string; -} - -export function emitContractPreview( - preview: ContractProjectionOptions["preview"], - runId: string, - sessionId: string, - update: PreviewUpdate, -): void { - try { - preview({ runId, sessionId, update }); - } catch { - // Preview is best-effort; retained state repairs a dropped callback. - } -} - -export const DEFAULT_PREVIEW_CHECKPOINT_BYTES = 128 * 1_024; -export const DEFAULT_PREVIEW_REPLACE_INTERVAL_MS = 1_000; - -export interface PreviewStreamState { - bytes: number; - lastReplaceAtMs: number; - mode: "append" | "replace"; - segment: number; - sequence: number; - text: string; -} - -export type TextPreviewChannel = - | "message.text" - | "reasoning.text" - | "terminal.stderr" - | "terminal.stdout"; - -export interface AppendTextInput { - readonly cause: MutationCause; - readonly channel: TextPreviewChannel; - readonly delta: string; - readonly event: string; - readonly itemId: string; - readonly runId: string; -} - -export interface CheckpointTextInput { - readonly cause: MutationCause; - readonly channel: TextPreviewChannel; - readonly event: string; - readonly itemId: string; - readonly runId: string; -} - -export interface ReplacePreviewInput { - readonly channel: PreviewUpdate["channel"]; - readonly itemId: string; - readonly runId: string; - readonly text: string; -} - -export function itemKey(runId: string, itemId: string): string { - return `${runId}\u0000${itemId}`; -} - -export function streamKey(runId: string, itemId: string, channel: string): string { - return `${itemKey(runId, itemId)}\u0000${channel}`; -} - -export function replaceCheckpointCause( - runId: string, - itemId: string, - segment: number, -): MutationCause { - return { - providerEventId: `preview/replace:${runId}:${segment}:${itemId}`.slice(0, 256), - type: "provider", - }; -} - -export function latestTimestamp(previous: string, next: string): string { - return compareTimestamps(previous, next) > 0 ? previous : next; -} - -function textContent(text: string): ContentBlock[] { - return text.length === 0 ? [] : [{ text, type: "text" }]; -} - -export function appendItemText( - item: Item, - channel: TextPreviewChannel, - text: string, - updatedAt: string, -): Item { - if (item.kind === "message" && channel === "message.text") { - return itemSchema.parse({ - ...item, - content: [...item.content, ...textContent(text)], - updatedAt: latestTimestamp(item.updatedAt, updatedAt), - }); - } - - if (item.kind === "reasoning" && channel === "reasoning.text") { - return itemSchema.parse({ - ...item, - content: [...item.content, ...textContent(text)], - updatedAt: latestTimestamp(item.updatedAt, updatedAt), - }); - } - - if (item.kind === "terminal" && channel === "terminal.stdout") { - return itemSchema.parse({ - ...item, - stdout: [...item.stdout, ...textContent(text)], - updatedAt: latestTimestamp(item.updatedAt, updatedAt), - }); - } - - if (item.kind === "terminal" && channel === "terminal.stderr") { - return itemSchema.parse({ - ...item, - stderr: [...item.stderr, ...textContent(text)], - updatedAt: latestTimestamp(item.updatedAt, updatedAt), - }); - } - - return item; -} - -export function advancePreviews( - previews: Map, - runId: string, - itemId: string, - atMs: number, -): void { - const prefix = `${itemKey(runId, itemId)}\u0000`; - - for (const [key, preview] of previews) { - if (!key.startsWith(prefix)) { - continue; - } - - previews.set(key, { - bytes: 0, - lastReplaceAtMs: atMs, - mode: preview.mode, - segment: preview.segment + 1, - sequence: 0, - text: "", - }); - } -} - -export function flushItemText( - previews: ReadonlyMap, - item: Item, - updatedAt: string, -): Item { - let next = item; - - for (const channel of [ - "message.text", - "reasoning.text", - "terminal.stdout", - "terminal.stderr", - ] as const) { - const preview = previews.get(streamKey(item.runId, item.id, channel)); - if (preview?.mode === "replace" && preview.sequence > 0) { - next = replaceItemText(next, channel, preview.text, updatedAt); - } else if (preview?.text !== undefined && preview.text.length > 0) { - next = appendItemText(next, channel, preview.text, updatedAt); - } - } - - return next; -} - -export function replaceItemText( - item: Item, - channel: TextPreviewChannel, - text: string, - updatedAt: string, -): Item { - if (item.kind === "terminal" && channel === "terminal.stdout") { - return itemSchema.parse({ - ...item, - stdout: textContent(text), - updatedAt: latestTimestamp(item.updatedAt, updatedAt), - }); - } - - if (item.kind === "terminal" && channel === "terminal.stderr") { - return itemSchema.parse({ - ...item, - stderr: textContent(text), - updatedAt: latestTimestamp(item.updatedAt, updatedAt), - }); - } - - return appendItemText(item, channel, text, updatedAt); -} - -export function truncateUtf8(value: string, maxBytes: number): string { - const encoded = new TextEncoder().encode(value); - return new TextDecoder().decode(encoded.subarray(0, maxBytes), { stream: true }); -} - -export function itemText(item: Item, channel: TextPreviewChannel): string { - if (item.kind === "message" && channel === "message.text") { - return item.content.flatMap((block) => (block.type === "text" ? [block.text] : [])).join(""); - } - - if (item.kind === "reasoning" && channel === "reasoning.text") { - return item.content.flatMap((block) => (block.type === "text" ? [block.text] : [])).join(""); - } - - if (item.kind === "terminal" && channel === "terminal.stdout") { - return item.stdout.flatMap((block) => (block.type === "text" ? [block.text] : [])).join(""); - } - - if (item.kind === "terminal" && channel === "terminal.stderr") { - return item.stderr.flatMap((block) => (block.type === "text" ? [block.text] : [])).join(""); - } - - return ""; -} - -export function matchesPreviewChannel(item: Item, channel: PreviewUpdate["channel"]): boolean { - switch (channel) { - case "message.text": - return item.kind === "message"; - case "reasoning.text": - return item.kind === "reasoning"; - case "terminal.stderr": - case "terminal.stdout": - return item.kind === "terminal"; - case "tool.progress": - return item.kind === "tool"; - default: - return true; - } -} diff --git a/src/runtimes/contract-projection-state.ts b/src/runtimes/contract-projection-state.ts deleted file mode 100644 index a55fd58..0000000 --- a/src/runtimes/contract-projection-state.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import { runSchema } from "../contract"; -import type { AuthorityOperation, Interaction, Item, Run } from "../contract"; -import { itemKey, latestTimestamp } from "./contract-projection-preview"; - -export interface ContractProjectionStateCallbacks { - readonly activeItem: (item: Item) => void; - readonly clearItem: (runId: string, itemId: string) => void; - readonly releaseRun: (runId: string) => void; -} - -export class ContractProjectionState { - readonly #childEndedAt = new Map(); - readonly #interactions = new Map(); - readonly #items = new Map(); - readonly #runs = new Map(); - - run(runId: string): Run | undefined { - return this.#runs.get(runId); - } - - item(runId: string, id: string): Item | undefined { - return this.#items.get(itemKey(runId, id)); - } - - interaction(id: string): Interaction | undefined { - return this.#interactions.get(id); - } - - releaseInteraction(id: string): void { - this.#interactions.delete(id); - } - - items(runId: string): Item[] { - return [...this.#items.values()].filter((item) => item.runId === runId); - } - - interactions(runId: string): Interaction[] { - return [...this.#interactions.values()].filter((interaction) => interaction.runId === runId); - } - - attachRun(value: Run): void { - const run = runSchema.parse(value); - - if (run.status !== "active") { - throw new Error(`Contract projection can only attach an active run ${run.id}.`); - } - - const existing = this.#runs.get(run.id); - - if (existing !== undefined) { - if (!isDeepStrictEqual(existing, run)) { - throw new Error( - `Contract projection run ${run.id} is already attached with different state.`, - ); - } - - return; - } - - this.#runs.set(run.id, run); - } - - latestChildEnd(runId: string): string | undefined { - return this.#childEndedAt.get(runId); - } - - requireRun(runId: string): Run { - const run = this.#runs.get(runId); - - if (run === undefined) { - throw new Error(`Contract projection references unknown run ${runId}.`); - } - - return run; - } - - apply( - operations: readonly AuthorityOperation[], - callbacks: ContractProjectionStateCallbacks, - ): void { - const terminalRuns: string[] = []; - - for (const operation of operations) { - if (operation.op === "remove") { - terminalRuns.push(operation.id); - continue; - } - - switch (operation.entity) { - case "session": - break; - case "run": - this.#runs.set(operation.value.id, operation.value); - if (operation.value.status !== "active") { - terminalRuns.push(operation.value.id); - } - break; - case "item": { - const item = operation.value; - this.#items.set(itemKey(item.runId, item.id), item); - if (item.status === "active") { - callbacks.activeItem(item); - } else { - callbacks.clearItem(item.runId, item.id); - } - break; - } - case "interaction": - this.#interactions.set(operation.value.id, operation.value); - break; - } - } - - for (const runId of terminalRuns) { - this.#releaseRun(runId); - callbacks.releaseRun(runId); - } - } - - clear(): void { - this.#childEndedAt.clear(); - this.#interactions.clear(); - this.#items.clear(); - this.#runs.clear(); - } - - #releaseRun(runId: string): void { - const run = this.#runs.get(runId); - - if (run !== undefined && run.status !== "active" && run.parentRunId !== undefined) { - const parent = this.#runs.get(run.parentRunId); - - if (parent?.status === "active") { - const previous = this.#childEndedAt.get(parent.id); - this.#childEndedAt.set( - parent.id, - previous === undefined ? run.endedAt : latestTimestamp(previous, run.endedAt), - ); - } - } - - this.#childEndedAt.delete(runId); - this.#runs.delete(runId); - - for (const key of this.#items.keys()) { - if (key.startsWith(`${runId}\u0000`)) { - this.#items.delete(key); - } - } - - for (const [id, interaction] of this.#interactions) { - if (interaction.runId === runId) { - this.#interactions.delete(id); - } - } - } -} diff --git a/src/runtimes/contract-projection.ts b/src/runtimes/contract-projection.ts deleted file mode 100644 index 399f850..0000000 --- a/src/runtimes/contract-projection.ts +++ /dev/null @@ -1,782 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import { interactionSchema, itemSchema, jsonByteLength, runSchema } from "../contract"; -import type { - AuthorityOperation, - Interaction, - Item, - MutationCause, - PreviewUpdate, - ProtocolError, - Run, - TokenUsage, -} from "../contract"; -import { - authorityKey, - ContractProjectionAuthority, - MAX_PENDING_MUTATION_BYTES, - MAX_PENDING_MUTATIONS, - type QueuedMutation, -} from "./contract-projection-authority"; -import { - advancePreviews, - appendItemText, - type AppendTextInput, - type CheckpointTextInput, - DEFAULT_PREVIEW_CHECKPOINT_BYTES, - DEFAULT_PREVIEW_REPLACE_INTERVAL_MS, - emitContractPreview, - flushItemText, - itemKey, - itemText, - latestTimestamp, - matchesPreviewChannel, - type ContractProjectionOptions, - type PreviewStreamState, - replaceCheckpointCause, - type ReplacePreviewInput, - replaceItemText, - streamKey, - type TextPreviewChannel, - truncateUtf8, -} from "./contract-projection-preview"; -import { ContractProjectionState } from "./contract-projection-state"; - -export { AuthorityOutcomeUnknownError } from "../contract"; -export { asJsonValue, createProviderMeta, nonEmpty } from "./contract-adapter-meta"; -export type { ContractAuthorityUpdate } from "./contract-projection-authority"; -export type { - ContractPreviewUpdate, - ContractProjectionOptions, -} from "./contract-projection-preview"; - -export class ContractProjection { - readonly #authority: ContractProjectionAuthority; - #disposed = false; - readonly #now: () => Date; - readonly #preview: ContractProjectionOptions["preview"]; - readonly #previewCheckpointBytes: number; - readonly #previewReplaceIntervalMs: number; - readonly #previewStreams = new Map(); - readonly #state = new ContractProjectionState(); - readonly #sessionId: string; - readonly #textEncoder = new TextEncoder(); - #mutationActive = false; - #mutationBytes = 0; - readonly #mutationQueue: QueuedMutation[] = []; - - constructor(options: ContractProjectionOptions) { - const admissionLimits = - options.admissionLimits === undefined ? undefined : { ...options.admissionLimits }; - this.#now = options.now ?? (() => new Date()); - this.#preview = options.preview; - this.#previewCheckpointBytes = - options.previewCheckpointBytes ?? DEFAULT_PREVIEW_CHECKPOINT_BYTES; - this.#previewReplaceIntervalMs = - options.previewReplaceIntervalMs ?? DEFAULT_PREVIEW_REPLACE_INTERVAL_MS; - this.#sessionId = options.sessionId; - - if ( - [ - this.#previewCheckpointBytes, - this.#previewReplaceIntervalMs, - ...(admissionLimits === undefined - ? [] - : [admissionLimits.maxBytes, admissionLimits.maxInlineBytes]), - ].some((value) => !Number.isSafeInteger(value) || value < 1) - ) { - throw new RangeError("Contract projection limits must be finite and positive."); - } - - this.#authority = new ContractProjectionAuthority({ - active: () => !this.#disposed, - admissionLimits, - apply: (operations) => this.#applyAuthorityOperations(operations), - authority: options.authority, - sessionId: options.sessionId, - }); - } - - now(): Date { - this.#assertActive(); - return this.#now(); - } - - run(runId: string): Run | undefined { - this.#assertActive(); - return this.#state.run(runId); - } - - item(runId: string, id: string): Item | undefined { - this.#assertActive(); - return this.#state.item(runId, id); - } - - interaction(id: string): Interaction | undefined { - this.#assertActive(); - return this.#state.interaction(id); - } - - releaseInteraction(id: string): void { - this.#assertActive(); - this.#state.releaseInteraction(id); - } - - items(runId: string): Item[] { - this.#assertActive(); - return this.#state.items(runId); - } - - interactions(runId: string): Interaction[] { - this.#assertActive(); - return this.#state.interactions(runId); - } - - attachRun(value: Run): void { - this.#assertActive(); - this.#state.attachRun(value); - } - - async putItem(runId: string, event: string, cause: MutationCause, value: Item): Promise { - const intent = { cause, event, runId, value }; - return this.#mutate(authorityKey(runId, event, cause), intent, (stable) => - this.#putItem(stable.runId, stable.event, stable.cause, stable.value, stable, false), - ); - } - - async #putItem( - runId: string, - event: string, - cause: MutationCause, - value: Item, - intent: unknown, - reuseDerived: boolean, - ): Promise { - this.#requireRun(runId); - const item = itemSchema.parse(value); - - if (item.runId !== runId) { - throw new Error(`Contract projection item ${item.id} belongs to a different run.`); - } - - const existing = this.#state.item(runId, item.id); - - if (existing !== undefined && isDeepStrictEqual(existing, item)) { - return existing; - } - - const operations = await this.#commit( - runId, - event, - cause, - [{ entity: "item", op: "put", value: item }], - intent, - reuseDerived, - ); - const operation = operations[0]; - - if (operation?.op !== "put" || operation.entity !== "item") { - throw new Error(`Authority write ${event} did not retain its Item intent.`); - } - - return this.#state.item(runId, operation.value.id) ?? operation.value; - } - - async putInteraction( - runId: string, - event: string, - cause: MutationCause, - value: Interaction, - ): Promise { - const intent = { cause, event, runId, value }; - return this.#mutate(authorityKey(runId, event, cause), intent, async (stable) => { - this.#requireRun(stable.runId); - const interaction = interactionSchema.parse(stable.value); - - if (interaction.runId !== stable.runId) { - throw new Error( - `Contract projection interaction ${interaction.id} belongs to a different run.`, - ); - } - - const existing = this.#state.interaction(interaction.id); - - if (existing !== undefined && isDeepStrictEqual(existing, interaction)) { - return existing; - } - - const operations = await this.#commit( - stable.runId, - stable.event, - stable.cause, - [{ entity: "interaction", op: "put", value: interaction }], - stable, - ); - const operation = operations[0]; - - if (operation?.op !== "put" || operation.entity !== "interaction") { - throw new Error(`Authority write ${stable.event} did not retain its Interaction intent.`); - } - - return this.#state.interaction(operation.value.id) ?? operation.value; - }); - } - - async updateUsage( - runId: string, - event: string, - cause: MutationCause, - usage: TokenUsage, - ): Promise { - const intent = { cause, event, runId, usage }; - return this.#mutate(authorityKey(runId, event, cause), intent, async (stable) => { - const current = this.#requireRun(stable.runId); - - if (current.status !== "active") { - return; - } - - const run = runSchema.parse({ ...current, usage: stable.usage }); - - if (isDeepStrictEqual(current, run)) { - return; - } - - const operations = await this.#commit( - stable.runId, - stable.event, - stable.cause, - [{ entity: "run", op: "put", value: run }], - stable, - ); - const operation = operations[0]; - - if (operation?.op !== "put" || operation.entity !== "run") { - throw new Error(`Authority write ${stable.event} did not retain its Run intent.`); - } - }); - } - - async appendText(input: AppendTextInput): Promise { - return this.#mutate( - authorityKey(input.runId, `${input.event}.checkpoint`, input.cause), - input, - async (input) => { - this.#assertActive(); - - if (input.delta.length === 0) { - return; - } - - const item = this.#state.item(input.runId, input.itemId); - - if ( - item === undefined || - item.status !== "active" || - !matchesPreviewChannel(item, input.channel) - ) { - return; - } - - const key = streamKey(input.runId, input.itemId, input.channel); - const now = this.#now(); - const current: PreviewStreamState = this.#previewStreams.get(key) ?? { - bytes: 0, - lastReplaceAtMs: now.getTime(), - mode: "append", - segment: 0, - sequence: 0, - text: "", - }; - - if (current.mode !== "append") { - throw new Error(`Preview stream ${input.channel} changed update mode.`); - } - const text = current.text + input.delta; - const bytes = current.bytes + this.#textEncoder.encode(input.delta).byteLength; - - if (bytes >= this.#previewCheckpointBytes) { - const checkpoint = appendItemText( - flushItemText(this.#previewStreams, item, now.toISOString()), - input.channel, - input.delta, - now.toISOString(), - ); - await this.#putItem( - input.runId, - `${input.event}.checkpoint`, - input.cause, - checkpoint, - input, - true, - ); - if (this.#disposed) { - return; - } - this.#previewStreams.set(key, { - bytes: 0, - lastReplaceAtMs: now.getTime(), - mode: "append", - segment: current.segment + 1, - sequence: 0, - text: "", - }); - return; - } - - const sequence = current.sequence + 1; - const replace = now.getTime() - current.lastReplaceAtMs >= this.#previewReplaceIntervalMs; - const update: PreviewUpdate = replace - ? { - channel: input.channel, - itemId: input.itemId, - op: "replace", - segment: current.segment, - streamId: input.channel, - text, - throughSequence: sequence, - } - : { - channel: input.channel, - fromSequence: sequence, - itemId: input.itemId, - op: "append", - segment: current.segment, - streamId: input.channel, - text: input.delta, - throughSequence: sequence, - }; - this.#previewStreams.set(key, { - bytes, - lastReplaceAtMs: replace ? now.getTime() : current.lastReplaceAtMs, - mode: "append", - segment: current.segment, - sequence, - text, - }); - this.#emitPreview(input.runId, update); - }, - ); - } - - async checkpointText(input: CheckpointTextInput): Promise { - return this.#mutate( - authorityKey(input.runId, input.event, input.cause), - input, - async (input) => { - this.#assertActive(); - const item = this.#state.item(input.runId, input.itemId); - - if ( - item === undefined || - item.status !== "active" || - !matchesPreviewChannel(item, input.channel) - ) { - return item; - } - - const key = streamKey(input.runId, input.itemId, input.channel); - const current = this.#previewStreams.get(key); - - if (current === undefined || current.text.length === 0) { - return item; - } - - if (current.mode !== "append") { - throw new Error(`Preview stream ${input.channel} changed update mode.`); - } - - const now = this.#now(); - const checkpoint = flushItemText(this.#previewStreams, item, now.toISOString()); - return this.#putItem(input.runId, input.event, input.cause, checkpoint, input, true); - }, - ); - } - - async replacePreview(input: ReplacePreviewInput): Promise { - return this.#mutate( - (stable) => { - const key = streamKey(stable.runId, stable.itemId, stable.channel); - const segment = (this.#previewStreams.get(key)?.segment ?? 0) + 1; - return authorityKey( - stable.runId, - "preview/replace.checkpoint", - replaceCheckpointCause(stable.runId, stable.itemId, segment), - ); - }, - input, - async (input) => { - this.#assertActive(); - const item = this.#state.item(input.runId, input.itemId); - - if ( - item === undefined || - item.status !== "active" || - !matchesPreviewChannel(item, input.channel) - ) { - return; - } - - const key = streamKey(input.runId, input.itemId, input.channel); - const current = this.#previewStreams.get(key); - if (current !== undefined && current.mode !== "replace") { - throw new Error(`Preview stream ${input.channel} changed update mode.`); - } - - const bytes = this.#textEncoder.encode(input.text).byteLength; - const canCheckpoint = - item.kind === "terminal" && - (input.channel === "terminal.stdout" || input.channel === "terminal.stderr"); - - if (bytes >= this.#previewCheckpointBytes && canCheckpoint) { - const now = this.#now(); - const nextSegment = (current?.segment ?? 0) + 1; - - if (itemText(item, input.channel) === input.text) { - this.#previewStreams.set(key, { - bytes: 0, - lastReplaceAtMs: now.getTime(), - mode: "replace", - segment: nextSegment, - sequence: 0, - text: "", - }); - return; - } - - const checkpoint = replaceItemText( - flushItemText(this.#previewStreams, item, now.toISOString()), - input.channel, - input.text, - now.toISOString(), - ); - await this.#putItem( - input.runId, - "preview/replace.checkpoint", - replaceCheckpointCause(input.runId, input.itemId, nextSegment), - checkpoint, - input, - true, - ); - if (this.#disposed) { - return; - } - this.#previewStreams.set(key, { - bytes: 0, - lastReplaceAtMs: now.getTime(), - mode: "replace", - segment: nextSegment, - sequence: 0, - text: "", - }); - return; - } - - const text = - bytes < this.#previewCheckpointBytes - ? input.text - : truncateUtf8(input.text, this.#previewCheckpointBytes - 1); - const sequence = (current?.sequence ?? 0) + 1; - const update = { - channel: input.channel, - itemId: input.itemId, - op: "replace", - segment: current?.segment ?? 0, - streamId: input.channel, - text, - throughSequence: sequence, - } satisfies PreviewUpdate; - this.#previewStreams.set(key, { - bytes: this.#textEncoder.encode(text).byteLength, - lastReplaceAtMs: this.#now().getTime(), - mode: "replace", - segment: current?.segment ?? 0, - sequence, - text, - }); - this.#emitPreview(input.runId, update); - }, - ); - } - - #emitPreview(runId: string, update: PreviewUpdate): void { - emitContractPreview(this.#preview, runId, this.#sessionId, update); - } - - materializedText(runId: string, itemId: string, channel: TextPreviewChannel): string { - this.#assertActive(); - const item = this.#state.item(runId, itemId); - const committed = item === undefined ? "" : itemText(item, channel); - const preview = this.#previewStreams.get(streamKey(runId, itemId, channel)); - - if (preview?.mode === "replace") { - return preview.sequence === 0 ? committed : preview.text; - } - - return committed + (preview?.text ?? ""); - } - - clearPreviews(runId: string, itemId: string): void { - this.#assertActive(); - const prefix = `${itemKey(runId, itemId)}\u0000`; - - for (const key of this.#previewStreams.keys()) { - if (key.startsWith(prefix)) { - this.#previewStreams.delete(key); - } - } - } - - async finishRun(input: { - readonly activeItemStatus?: "cancelled" | "completed" | undefined; - readonly cause: MutationCause; - readonly endedAt?: string | undefined; - readonly error?: ProtocolError | undefined; - readonly event: string; - readonly finishReason?: "success" | "limit" | "refusal" | "other" | undefined; - readonly terminalItems?: readonly Item[] | undefined; - readonly reason?: string | undefined; - readonly runId: string; - readonly status: "cancelled" | "completed" | "failed"; - }): Promise { - return this.#mutate( - authorityKey(input.runId, input.event, input.cause), - input, - async (input) => { - const current = this.#requireRun(input.runId); - - if (current.status !== "active") { - return; - } - - const terminalItems = (input.terminalItems ?? []).map((value) => itemSchema.parse(value)); - const terminalItemIds = new Set(); - - for (const item of terminalItems) { - if ( - item.runId !== input.runId || - item.status === "active" || - terminalItemIds.has(item.id) - ) { - throw new Error(`Contract projection received an invalid terminal Item ${item.id}.`); - } - - terminalItemIds.add(item.id); - } - - const currentItems = this.items(input.runId); - const requestedEnd = input.endedAt ?? this.#now().toISOString(); - const endedAt = this.interactions(input.runId).reduce( - (latest, interaction) => - latestTimestamp(latest, interaction.endedAt ?? interaction.createdAt), - [ - ...currentItems.filter((item) => !terminalItemIds.has(item.id)), - ...terminalItems, - ].reduce( - (latest, item) => - latestTimestamp( - latest, - latestTimestamp(item.updatedAt, item.endedAt ?? item.updatedAt), - ), - latestTimestamp( - latestTimestamp(current.startedAt, requestedEnd), - this.#state.latestChildEnd(input.runId) ?? current.startedAt, - ), - ), - ); - const error = input.error ?? { - code: "provider.run_failed", - message: "Provider run failed.", - retryable: false, - }; - const items = currentItems.flatMap((item): Item[] => { - if (item.status !== "active" || terminalItemIds.has(item.id)) { - return []; - } - - const withText = flushItemText(this.#previewStreams, item, endedAt); - return [ - itemSchema.parse( - input.status === "completed" - ? { - ...withText, - endedAt, - status: input.activeItemStatus ?? "completed", - updatedAt: endedAt, - } - : input.status === "cancelled" - ? { ...withText, endedAt, status: "cancelled", updatedAt: endedAt } - : { ...withText, endedAt, error, status: "failed", updatedAt: endedAt }, - ), - ]; - }); - const interactions = this.interactions(input.runId).flatMap( - (interaction): Interaction[] => { - if (interaction.status !== "open") { - return []; - } - - return [ - interactionSchema.parse({ - ...interaction, - endedAt, - status: "expired", - }), - ]; - }, - ); - const run = runSchema.parse( - input.status === "completed" - ? { - ...current, - endedAt, - finishReason: input.finishReason ?? "success", - status: "completed", - } - : input.status === "cancelled" - ? { - ...current, - endedAt, - ...(input.reason === undefined ? {} : { reason: input.reason }), - status: "cancelled", - } - : { ...current, endedAt, error, status: "failed" }, - ); - const operations: AuthorityOperation[] = [ - ...terminalItems.map((value) => ({ - entity: "item", - op: "put", - value, - })), - ...items.map((value) => ({ entity: "item", op: "put", value })), - ...interactions.map((value) => ({ - entity: "interaction", - op: "put", - value, - })), - { entity: "run", op: "put", value: run }, - ]; - await this.#commit(input.runId, input.event, input.cause, operations, input, true); - }, - ); - } - - dispose(): void { - this.#disposed = true; - const error = new Error("Contract projection is disposed."); - - for (const mutation of this.#mutationQueue.splice(0)) { - this.#mutationBytes -= mutation.bytes; - mutation.reject(error); - } - - this.#state.clear(); - this.#previewStreams.clear(); - this.#authority.clear(); - } - - #mutate( - key: string | ((intent: I) => string), - intent: I, - operation: (intent: I) => Promise | T, - ): Promise { - this.#assertActive(); - const bytes = jsonByteLength(intent); - const pending = this.#mutationQueue.length + (this.#mutationActive ? 1 : 0); - - if (pending >= MAX_PENDING_MUTATIONS) { - return Promise.reject( - new RangeError( - `Contract projection mutation queue exceeds ${MAX_PENDING_MUTATIONS} entries.`, - ), - ); - } - - if (bytes > MAX_PENDING_MUTATION_BYTES - this.#mutationBytes) { - return Promise.reject( - new RangeError( - `Contract projection mutation queue exceeds ${MAX_PENDING_MUTATION_BYTES} UTF-8 bytes.`, - ), - ); - } - - const stableIntent = structuredClone(intent); - const unknownAtEnqueue = this.#authority.unknown; - const task = Promise.withResolvers(); - this.#mutationQueue.push({ - bytes, - reject: task.reject, - run: async () => { - try { - this.#assertActive(); - const resolvedKey = typeof key === "function" ? key(stableIntent) : key; - this.#authority.assertRetry(resolvedKey, stableIntent, unknownAtEnqueue); - - task.resolve(await operation(stableIntent)); - } catch (error) { - task.reject(error); - } - }, - }); - this.#mutationBytes += bytes; - this.#drainMutations(); - return task.promise; - } - - #drainMutations(): void { - if (this.#mutationActive) { - return; - } - - // ponytail: the 1,024-entry hard cap keeps a native Array FIFO sufficient. - const mutation = this.#mutationQueue.shift(); - if (mutation === undefined) { - return; - } - - this.#mutationActive = true; - void mutation.run().finally(() => { - this.#mutationBytes -= mutation.bytes; - this.#mutationActive = false; - this.#drainMutations(); - }); - } - - async #commit( - runId: string, - event: string, - cause: MutationCause, - operations: readonly AuthorityOperation[], - intent: unknown, - reuseDerived = false, - ): Promise { - this.#assertActive(); - return this.#authority.commit(runId, event, cause, operations, intent, reuseDerived); - } - - #applyAuthorityOperations(operations: readonly AuthorityOperation[]): void { - this.#state.apply(operations, { - activeItem: (item) => - advancePreviews(this.#previewStreams, item.runId, item.id, Date.parse(item.updatedAt)), - clearItem: (runId, itemId) => this.clearPreviews(runId, itemId), - releaseRun: (runId) => { - for (const key of this.#previewStreams.keys()) { - if (key.startsWith(`${runId}\u0000`)) { - this.#previewStreams.delete(key); - } - } - }, - }); - } - - #requireRun(runId: string): Run { - this.#assertActive(); - return this.#state.requireRun(runId); - } - - #assertActive(): void { - if (this.#disposed) { - throw new Error("Contract projection is disposed."); - } - } -} diff --git a/src/runtimes/driver-event-admission.ts b/src/runtimes/driver-event-admission.ts index 6187d4c..47e4481 100644 --- a/src/runtimes/driver-event-admission.ts +++ b/src/runtimes/driver-event-admission.ts @@ -4,7 +4,6 @@ import type { RunId } from "../protocol/id"; const MAX_PENDING_DRIVER_EVENTS = 1_024; const MAX_PENDING_DRIVER_EVENT_BYTES = 32 * 1_024 * 1_024; -const MAX_RUN_TERMINAL_BATCH_EVENTS = 64; const MAX_RUN_TERMINAL_BATCH_BYTES = 1_024 * 1_024; export interface QueuedDriverEvent { @@ -35,6 +34,31 @@ export interface AdmittedDriverEventPush { readonly terminalKey: string | null; } +const EMPTY_DRIVER_EVENT_ADMISSION_STATE: DriverEventAdmissionState = { + pendingLosslessBytes: 0, + pendingLosslessCount: 0, + pendingTerminalBatch: false, + queuedEventBytes: 0, + queuedEventCount: 0, + queuedLosslessBytes: 0, + queuedLosslessCount: 0, + queuedTerminalBatches: 0, +}; + +export function preflightDriverEventPush( + events: readonly DriverEventInput[], + activeRunId: RunId | null, +): void { + for (const event of events) { + admitDriverEventPush( + [event], + activeRunId, + Symbol("driver-event-preflight"), + EMPTY_DRIVER_EVENT_ADMISSION_STATE, + ); + } +} + export function driverEventBatchBytes(bytes: number, count: number): number { return count === 0 ? 0 : bytes + count + 1; } @@ -43,13 +67,28 @@ export function isLosslessDriverEvent(event: DriverEventInput): boolean { return event.delivery !== "best_effort"; } -function isRunTerminal(event: DriverEventInput): boolean { +export function losslessDriverEventRetryKey( + event: DriverEventInput, + activeRunId: RunId | null, +): string | null { + if (!isLosslessDriverEvent(event)) { + return null; + } + + const { sourceEventId: _, ...scoped } = scopeDriverEvent(event, activeRunId); + return JSON.stringify(scoped); +} + +export function isRunTerminalDriverEvent(event: DriverEventInput): boolean { return ( event.kind === "run.cancelled" || event.kind === "run.completed" || event.kind === "run.failed" ); } -function scopeEvent(event: DriverEventInput, activeRunId: RunId | null): DriverEventInput { +export function scopeDriverEvent( + event: DriverEventInput, + activeRunId: RunId | null, +): DriverEventInput { const { runId, sourceEventId, ...content } = event; const frozenRunId = runId === undefined ? activeRunId : runId; const explicitSourceId = @@ -58,7 +97,7 @@ function scopeEvent(event: DriverEventInput, activeRunId: RunId | null): DriverE return { ...content, ...(explicitSourceId === null ? {} : { sourceEventId: explicitSourceId }), - ...(frozenRunId === null ? {} : { runId: frozenRunId }), + ...(runId === null ? { runId: null } : frozenRunId === null ? {} : { runId: frozenRunId }), } as DriverEventInput; } @@ -70,14 +109,13 @@ export function terminalDriverEventRetryKey( if ( losslessEvents.length === 0 || - losslessEvents.length > MAX_RUN_TERMINAL_BATCH_EVENTS || - losslessEvents.filter(isRunTerminal).length !== 1 || - !isRunTerminal(losslessEvents.at(-1)!) + losslessEvents.filter(isRunTerminalDriverEvent).length !== 1 || + !isRunTerminalDriverEvent(losslessEvents.at(-1)!) ) { return null; } - const key = JSON.stringify(losslessEvents.map((event) => scopeEvent(event, activeRunId))); + const key = JSON.stringify(losslessEvents.map((event) => scopeDriverEvent(event, activeRunId))); return Buffer.byteLength(key, "utf8") <= MAX_RUN_TERMINAL_BATCH_BYTES ? key : null; } @@ -88,7 +126,7 @@ export function admitDriverEventPush( state: DriverEventAdmissionState, ): AdmittedDriverEventPush | null { const losslessEvents = events.filter(isLosslessDriverEvent); - const runTerminals = losslessEvents.filter(isRunTerminal); + const runTerminals = losslessEvents.filter(isRunTerminalDriverEvent); const terminalBatch = runTerminals.length > 0; if (runTerminals.length > 1) { @@ -100,13 +138,7 @@ export function admitDriverEventPush( throw new Error("Driver event run terminal slot is full."); } - if (losslessEvents.length > MAX_RUN_TERMINAL_BATCH_EVENTS) { - throw new Error( - `Driver event run terminal batch exceeds ${MAX_RUN_TERMINAL_BATCH_EVENTS} events.`, - ); - } - - if (!isRunTerminal(losslessEvents.at(-1)!)) { + if (!isRunTerminalDriverEvent(losslessEvents.at(-1)!)) { throw new Error("Driver event run terminal must be the final lossless event."); } } @@ -141,9 +173,14 @@ export function admitDriverEventPush( admittedEvents = includeBestEffort ? events : losslessEvents; } - const frozenEvents = admittedEvents.map((event) => scopeEvent(event, activeRunId)); + const frozenEvents = admittedEvents.map((event) => scopeDriverEvent(event, activeRunId)); const terminalKey = terminalBatch ? JSON.stringify(frozenEvents) : null; const stampedEvents = withSourceEventIds(frozenEvents); + + if (new Set(stampedEvents.map((event) => event.sourceEventId)).size !== stampedEvents.length) { + throw new Error("Driver event push requires unique source event IDs."); + } + const serialized: (string | undefined)[] = []; const serializedBytes: (number | undefined)[] = []; let losslessByteSum = 0; diff --git a/src/runtimes/driver-event-publisher.ts b/src/runtimes/driver-event-publisher.ts index 495af93..d93fca2 100644 --- a/src/runtimes/driver-event-publisher.ts +++ b/src/runtimes/driver-event-publisher.ts @@ -3,24 +3,55 @@ import { DRIVER_EVENT_DELIVERY_TIMEOUT_MS, DriverEventRejectedError, pushLosslessEvents, + withSourceEventIds, } from "../core/driver-runtime-io"; +import { createHash } from "node:crypto"; import { summarizeDriverEventBatch } from "../observability/driver-debug"; import type { DriverEventInput } from "../protocol/events"; import type { RunId } from "../protocol/id"; import type { DriverEventReceipt } from "../protocol/orpc"; import type { DriverRuntime } from "../protocol/runtime"; +import { raceWithAbort } from "../utils/async"; import type { AgentDriverContext } from "../core/agent-driver-backend"; import { admitDriverEventPush, driverEventBatchBytes, isLosslessDriverEvent, + isRunTerminalDriverEvent, + losslessDriverEventRetryKey, + preflightDriverEventPush, + scopeDriverEvent, terminalDriverEventRetryKey, type QueuedDriverEvent, } from "./driver-event-admission"; +interface TerminalSettlement { + readonly acceptedSourceEventIds: Set; + readonly activeRunId: RunId; + readonly events: readonly DriverEventInput[]; + readonly key: string; + signal: AbortSignal; + nextIndex: number; + rejection: unknown | null; + task: Promise | null; +} + +function terminalSettlementKey(events: readonly DriverEventInput[]): string { + const hash = createHash("sha256"); + + for (const event of events) { + const json = JSON.stringify(event); + hash.update(String(Buffer.byteLength(json, "utf8"))); + hash.update(":"); + hash.update(json); + } + + return hash.digest("hex"); +} interface QueuedPush { readonly awaitPending: boolean; readonly context: AgentDriverContext; + readonly deliverySignal: AbortSignal | null; readonly eventBytes: number; readonly eventCount: number; readonly events: QueuedDriverEvent[]; @@ -28,6 +59,7 @@ interface QueuedPush { readonly losslessBytes: number; readonly losslessCount: number; readonly promise: Promise; + readonly retrySourceEventIds: readonly string[]; readonly reason: string; readonly reject: (reason?: unknown) => void; readonly resolve: () => void; @@ -35,10 +67,28 @@ interface QueuedPush { readonly terminalKey: string | null; } +class QueuedPushDeliveryError extends Error { + readonly deliveryCause: unknown; + readonly retainedSourceEventIds: readonly string[]; + + constructor(cause: unknown, retainedSourceEventIds: readonly string[]) { + super(cause instanceof Error ? cause.message : "Driver event delivery failed.", { cause }); + this.deliveryCause = cause; + this.name = "QueuedPushDeliveryError"; + this.retainedSourceEventIds = retainedSourceEventIds; + } +} + +function deliveryCause(error: unknown): unknown { + return error instanceof QueuedPushDeliveryError ? error.deliveryCause : error; +} + export class DriverEventPublisher { readonly #getSessionRef: () => string | null; readonly #runtime: DriverRuntime; + #acceptedInFlightSourceEventIds = new Set(); #drainTask: Promise | null = null; + #inFlightEvents: readonly QueuedDriverEvent[] | null = null; #lastAcceptedSeq = 0; #pendingEvents: QueuedDriverEvent[] = []; #pendingLosslessBytes = 0; @@ -52,6 +102,8 @@ export class DriverEventPublisher { #queuedLosslessBytes = 0; #queuedLosslessCount = 0; #queuedTerminalBatches = 0; + #rejectedInFlight = new Map(); + #terminalSettlement: TerminalSettlement | null = null; constructor(runtime: DriverRuntime, getSessionRef: () => string | null) { this.#getSessionRef = getSessionRef; @@ -62,36 +114,396 @@ export class DriverEventPublisher { context: AgentDriverContext, reason: string, events: readonly DriverEventInput[], + ): Promise { + if (events.some(isRunTerminalDriverEvent)) { + throw new Error("Driver run terminals must use pushTerminal()."); + } + + try { + return this.#submit(context, reason, events).catch((error: unknown) => { + throw deliveryCause(error); + }); + } catch (error) { + this.#requestPendingDrain(context, reason); + throw deliveryCause(error); + } + } + + async pushSession( + context: AgentDriverContext, + reason: string, + events: readonly DriverEventInput[], + ): Promise { + if (events.some((event) => event.runId !== undefined)) { + throw new Error("Driver session events cannot target a run."); + } + if (events.some(isRunTerminalDriverEvent)) { + throw new Error("Driver run terminals must use pushTerminal()."); + } + + await this.#terminalSettlement?.task; + const sessionEvents = events.map((event): DriverEventInput => ({ ...event, runId: null })); + try { + return this.#submit(context, reason, sessionEvents, undefined, true).catch( + (error: unknown) => { + throw deliveryCause(error); + }, + ); + } catch (error) { + this.#requestPendingDrain(context, reason); + throw deliveryCause(error); + } + } + + #submit( + context: AgentDriverContext, + reason: string, + events: readonly DriverEventInput[], + terminalSettlement?: TerminalSettlement, + sessionScoped = false, ): Promise { if (events.length === 0) { this.#requestPendingDrain(context, reason); - return; + return Promise.resolve(); + } + + const currentRunId = context.ports.eventSink.currentRunId(); + const activeRunId = sessionScoped ? null : currentRunId; + const explicitRunIds = new Set( + events.flatMap((event) => + event.runId === undefined || event.runId === null ? [] : [event.runId], + ), + ); + + if ( + terminalSettlement === undefined && + activeRunId !== null && + [...explicitRunIds].some((runId) => runId !== activeRunId) + ) { + if (events.some(isLosslessDriverEvent)) { + throw new Error("Driver event must target the active run."); + } + + return Promise.resolve(); + } + + const settlement = this.#terminalSettlement; + + if ( + settlement !== null && + settlement !== terminalSettlement && + (settlement.nextIndex >= settlement.events.length || settlement.task === null) && + currentRunId !== settlement.activeRunId + ) { + this.#terminalSettlement = null; + } + + const reserved = this.#terminalSettlement; + + if (reserved !== null && reserved !== terminalSettlement && !sessionScoped) { + if (events.some(isLosslessDriverEvent)) { + throw new Error( + `Driver event run terminal settlement slot is full (active=${String(activeRunId)}, reserved=${String(reserved.activeRunId)}, task=${reserved.task === null ? "idle" : "set"}).`, + ); + } + + return Promise.resolve(); } + if ( + this.#pendingTerminalKey !== null && + terminalDriverEventRetryKey(events, activeRunId) === this.#pendingTerminalKey + ) { + const retry = this.#createPendingRetry( + context, + reason, + undefined, + terminalSettlement?.signal, + ); + this.#enqueue(retry); + return retry.promise; + } + + if (this.#pendingTerminalBatch || this.#queuedTerminalBatches > 0) { + if (events.some(isLosslessDriverEvent)) { + throw new Error("Driver event run terminal slot is full."); + } + + this.#requestPendingDrain(context, reason); + return Promise.resolve(); + } + + const entry = this.#admit(context, reason, events, activeRunId, terminalSettlement?.signal); + + if (entry === null) { + this.#requestPendingDrain(context, reason); + return Promise.resolve(); + } + + if (terminalSettlement === undefined) { + const unresolvedSourceEventIds = new Set( + [ + ...(this.#inFlightEvents ?? this.#pendingEvents), + ...this.#queue.flatMap((queued) => queued.events), + ].map(({ event }) => event.sourceEventId), + ); + + if (entry.events.some(({ event }) => unresolvedSourceEventIds.has(event.sourceEventId))) { + throw new Error("Driver source event ID conflicts with a pending event."); + } + } + + this.#enqueue(entry); + return entry.losslessCount === 0 && !entry.terminalBatch ? Promise.resolve() : entry.promise; + } + + async #retryPending( + context: AgentDriverContext, + reason: string, + sourceEventIds: readonly string[], + deliverySignal?: AbortSignal, + ): Promise { + const retry = this.#createPendingRetry(context, reason, sourceEventIds, deliverySignal); + this.#enqueue(retry); try { - const activeRunId = context.ports.eventSink.currentRunId?.() ?? null; + await retry.promise; + } catch (error) { + throw deliveryCause(error); + } + } + + pushTerminal( + context: AgentDriverContext, + reason: string, + closures: readonly DriverEventInput[], + terminal: DriverEventInput, + ): Promise { + if (!isRunTerminalDriverEvent(terminal)) { + throw new Error("Driver terminal push requires a run terminal event."); + } + + if (!isLosslessDriverEvent(terminal)) { + throw new Error("Driver terminal push requires lossless events."); + } + + for (const closure of closures) { + if (!isLosslessDriverEvent(closure)) { + throw new Error("Driver terminal push requires lossless events."); + } + + if (isRunTerminalDriverEvent(closure)) { + throw new Error("Driver terminal closures cannot contain a run terminal event."); + } + } + + const activeRunId = context.ports.eventSink.currentRunId(); + const targetRunId = (terminal.runId as RunId | undefined) ?? activeRunId; + + if (activeRunId === null) { + throw new Error("Driver terminal push requires an active run."); + } + + const scopedEvents = [...closures, terminal].map((event) => + scopeDriverEvent(event, targetRunId), + ); + + if ( + scopedEvents.some((event) => ((event.runId as RunId | undefined) ?? null) !== targetRunId) + ) { + throw new Error("Driver terminal push requires every event to target the same run."); + } + + const key = terminalSettlementKey(scopedEvents); + const current = this.#terminalSettlement; + + if (current !== null) { + if (current.key === key && current.task !== null) { + return current.task; + } + + if (current.key === key) { + current.signal = AbortSignal.timeout(DRIVER_EVENT_DELIVERY_TIMEOUT_MS); + return this.#startTerminalSettlement(context, reason, current); + } + + const idle = current.nextIndex >= current.events.length || current.task === null; + const activeRunChanged = activeRunId !== current.activeRunId; + if (!idle || !activeRunChanged) { + throw new Error("Driver event run terminal settlement slot is full."); + } + + this.#terminalSettlement = null; + } + + if (targetRunId !== activeRunId) { + throw new Error("Driver terminal push must target the active run."); + } + + const retryCandidates = [ + ...(this.#inFlightEvents ?? this.#pendingEvents), + ...this.#queue.flatMap((entry) => entry.events), + ].filter( + ({ event }, index, candidates) => + candidates.findIndex( + ({ event: candidate }) => candidate.sourceEventId === event.sourceEventId, + ) === index, + ); + const adoptedPending = new Set(); + const frozenClosures = scopedEvents.slice(0, -1).map((event) => { + if (event.sourceEventId !== undefined) { + return event; + } + + const retryKey = losslessDriverEventRetryKey(event, targetRunId); + const pendingIndex = retryCandidates.findIndex( + ({ event: pending }, index) => + !adoptedPending.has(index) && + losslessDriverEventRetryKey(pending, targetRunId) === retryKey, + ); + + if (pendingIndex < 0) { + return event; + } + + adoptedPending.add(pendingIndex); + return { + ...event, + sourceEventId: retryCandidates[pendingIndex]!.event.sourceEventId, + }; + }); + const events = structuredClone(withSourceEventIds([...frozenClosures, scopedEvents.at(-1)!])); + + if (new Set(events.map((event) => event.sourceEventId)).size !== events.length) { + throw new Error("Driver terminal push requires unique source event IDs."); + } + + for (const [index, event] of events.entries()) { + const candidate = retryCandidates.find( + ({ event: pending }) => pending.sourceEventId === event.sourceEventId, + ); if ( - this.#pendingTerminalKey !== null && - terminalDriverEventRetryKey(events, activeRunId) === this.#pendingTerminalKey + candidate !== undefined && + (index === events.length - 1 || + losslessDriverEventRetryKey(candidate.event, targetRunId) !== + losslessDriverEventRetryKey(event, targetRunId)) ) { - const retry = this.#createPendingRetry(context, reason); - this.#enqueue(retry); - return retry.promise; + throw new Error("Driver terminal source event ID conflicts with a pending event."); } + } - const entry = this.#admit(context, reason, events, activeRunId); + preflightDriverEventPush(events, targetRunId); - if (entry === null) { - this.#requestPendingDrain(context, reason); - return; + if (this.#terminalSettlement !== null) { + if (this.#terminalSettlement.key !== key) { + throw new Error("Driver event run terminal settlement slot is full."); } + } - this.#enqueue(entry); - return entry.losslessCount === 0 && !entry.terminalBatch ? undefined : entry.promise; - } catch (error) { - this.#requestPendingDrain(context, reason); - throw error; + const rejectedSourceEventId = events.find( + ({ sourceEventId }) => + sourceEventId !== undefined && this.#rejectedInFlight.has(sourceEventId), + )?.sourceEventId; + const settlement: TerminalSettlement = { + acceptedSourceEventIds: new Set( + events + .map((event) => event.sourceEventId!) + .filter((sourceEventId) => this.#acceptedInFlightSourceEventIds.has(sourceEventId)), + ), + activeRunId, + events, + key, + nextIndex: 0, + rejection: + rejectedSourceEventId === undefined + ? null + : this.#rejectedInFlight.get(rejectedSourceEventId)!, + signal: AbortSignal.timeout(DRIVER_EVENT_DELIVERY_TIMEOUT_MS), + task: null, + }; + this.#terminalSettlement = settlement; + return this.#startTerminalSettlement(context, reason, settlement); + } + + #startTerminalSettlement( + context: AgentDriverContext, + reason: string, + settlement: TerminalSettlement, + ): Promise { + const task = this.#deliverTerminalSettlement(context, reason, settlement).catch( + (error: unknown) => { + if (settlement.task === task) { + settlement.task = null; + } + + throw error; + }, + ); + settlement.task = task; + return task; + } + + async #deliverTerminalSettlement( + context: AgentDriverContext, + reason: string, + settlement: TerminalSettlement, + ): Promise { + const priorDrain = this.#drainTask; + + if (priorDrain !== null) { + await raceWithAbort(priorDrain, settlement.signal); + } + + if (settlement.rejection !== null) { + throw settlement.rejection; + } + + this.#advanceTerminalSettlement(settlement); + + while (settlement.nextIndex < settlement.events.length) { + const terminal = settlement.nextIndex === settlement.events.length - 1; + const event = settlement.events[settlement.nextIndex]!; + const deliveryReason = terminal ? reason : `${reason}.items`; + const retryReason = `${deliveryReason}.retry`; + let delivery: Promise; + + try { + const retained = terminal + ? undefined + : this.#pendingEvents.find( + ({ event: pending }) => pending.sourceEventId === event.sourceEventId, + ); + delivery = + retained === undefined + ? this.#submit(context, deliveryReason, [event], settlement) + : this.#retryPending( + context, + retryReason, + [retained.event.sourceEventId!], + settlement.signal, + ); + } catch (error) { + this.#requestPendingDrain(context, deliveryReason); + throw error; + } + + try { + await delivery; + } catch (error) { + if ( + !(error instanceof QueuedPushDeliveryError) || + error.retainedSourceEventIds.length === 0 + ) { + throw deliveryCause(error); + } + + await this.#retryPending( + context, + retryReason, + error.retainedSourceEventIds, + settlement.signal, + ); + } } } @@ -109,12 +521,20 @@ export class DriverEventPublisher { return this.#lastAcceptedSeq; } - #createPendingRetry(context: AgentDriverContext, reason: string): QueuedPush { + #createPendingRetry( + context: AgentDriverContext, + reason: string, + sourceEventIds: readonly string[] = this.#pendingEvents.map( + ({ event }) => event.sourceEventId!, + ), + deliverySignal?: AbortSignal, + ): QueuedPush { const deferred = Promise.withResolvers(); return { awaitPending: true, context, + deliverySignal: deliverySignal ?? null, eventBytes: 0, eventCount: 0, events: [], @@ -125,6 +545,7 @@ export class DriverEventPublisher { reason, reject: deferred.reject, resolve: deferred.resolve, + retrySourceEventIds: sourceEventIds, terminalBatch: false, terminalKey: null, }; @@ -135,6 +556,7 @@ export class DriverEventPublisher { reason: string, events: readonly DriverEventInput[], activeRunId: RunId | null, + deliverySignal?: AbortSignal, ): QueuedPush | null { const id = Symbol(reason); const admission = admitDriverEventPush(events, activeRunId, id, { @@ -157,11 +579,13 @@ export class DriverEventPublisher { ...admission, awaitPending: false, context, + deliverySignal: deliverySignal ?? null, id, promise: deferred.promise, reason, reject: deferred.reject, resolve: deferred.resolve, + retrySourceEventIds: [], }; } @@ -215,7 +639,27 @@ export class DriverEventPublisher { context: AgentDriverContext, wakeReason?: string, ): Promise { + const retryDeliverySignals = new Map(); + const deliverySignalsByOwner = new Map(); + + for (const entry of entries) { + if (entry.deliverySignal === null) { + continue; + } + + deliverySignalsByOwner.set(entry.id, entry.deliverySignal); + + if (entry.awaitPending) { + for (const sourceEventId of entry.retrySourceEventIds) { + retryDeliverySignals.set(sourceEventId, entry.deliverySignal); + } + } + } + const queuedEvents = [...this.#pendingEvents, ...entries.flatMap((entry) => entry.events)]; + this.#inFlightEvents = queuedEvents; + this.#acceptedInFlightSourceEventIds = new Set(); + this.#rejectedInFlight = new Map(); const remainingLossless = queuedEvents.filter(({ event }) => isLosslessDriverEvent(event)); const ownerErrors = new Map(); const reason = @@ -252,9 +696,16 @@ export class DriverEventPublisher { } const sameDelivery = queuedEvents.slice(index, end); + const deliverySignal = sameDelivery + .map(({ event, owner }) => + owner === null + ? retryDeliverySignals.get(event.sourceEventId!) + : deliverySignalsByOwner.get(owner), + ) + .find((signal) => signal !== undefined); if (lossless) { - const deadline = AbortSignal.timeout(DRIVER_EVENT_DELIVERY_TIMEOUT_MS); + const deadline = deliverySignal ?? AbortSignal.timeout(DRIVER_EVENT_DELIVERY_TIMEOUT_MS); let remaining = sameDelivery; while (remaining.length > 0) { @@ -265,9 +716,12 @@ export class DriverEventPublisher { context.ports.eventSink, remaining.map(({ event }) => event), (receipts) => { + const acceptedEvents = remaining + .slice(acceptedInAttempt, acceptedInAttempt + receipts.length) + .map(({ event }) => event); acceptedInAttempt += receipts.length; acceptedEventCount += receipts.length; - this.#rememberAcceptedReceipts(receipts); + this.#rememberAcceptedReceipts(receipts, acceptedEvents); remainingLossless.splice(0, receipts.length); }, deadline, @@ -296,8 +750,30 @@ export class DriverEventPublisher { } remainingLossless.splice(retainedIndex, 1); + this.#rejectedInFlight.set(error.sourceEventId, error); - if (rejected!.owner !== null && !ownerErrors.has(rejected!.owner)) { + const settlement = this.#terminalSettlement; + + if ( + settlement !== null && + settlement.events.some( + (event) => event.sourceEventId === rejected!.event.sourceEventId, + ) + ) { + settlement.rejection ??= error; + } + + if (rejected!.owner === null) { + for (const entry of entries) { + if ( + entry.awaitPending && + entry.retrySourceEventIds.includes(error.sourceEventId) && + !ownerErrors.has(entry.id) + ) { + ownerErrors.set(entry.id, error); + } + } + } else if (!ownerErrors.has(rejected!.owner)) { ownerErrors.set(rejected!.owner, error); } } @@ -312,7 +788,10 @@ export class DriverEventPublisher { }); assertDriverEventReceiptPrefix(events, result.accepted); acceptedEventCount += result.accepted.length; - this.#rememberAcceptedReceipts(result.accepted); + this.#rememberAcceptedReceipts( + result.accepted, + events.slice(0, result.accepted.length), + ); } catch { // Best-effort events are intentionally dropped on transport or receipt failure. } @@ -337,10 +816,18 @@ export class DriverEventPublisher { }); for (const entry of entries) { + const retainedSourceEventIds = remainingLossless + .filter( + ({ event, owner }) => + owner === entry.id || + (entry.awaitPending && + owner === null && + entry.retrySourceEventIds.includes(event.sourceEventId!)), + ) + .map(({ event }) => event.sourceEventId!); const error = ownerErrors.get(entry.id) ?? - (remainingLossless.some(({ owner }) => owner === entry.id) || - (entry.awaitPending && remainingLossless.some(({ owner }) => owner === null)) + (retainedSourceEventIds.length > 0 ? (deliveryError ?? new Error("Driver event delivery did not settle.")) : null); @@ -353,9 +840,13 @@ export class DriverEventPublisher { if (error === null) { entry.resolve(); } else { - entry.reject(error); + entry.reject(new QueuedPushDeliveryError(error, retainedSourceEventIds)); } } + + this.#inFlightEvents = null; + this.#acceptedInFlightSourceEventIds.clear(); + this.#rejectedInFlight.clear(); } #setPending(events: readonly QueuedDriverEvent[], terminalKey: string | null): void { @@ -379,9 +870,47 @@ export class DriverEventPublisher { this.#pendingTerminalKey = terminalBatch ? terminalKey : null; } - #rememberAcceptedReceipts(receipts: readonly DriverEventReceipt[]): void { + #rememberAcceptedReceipts( + receipts: readonly DriverEventReceipt[], + events: readonly DriverEventInput[], + ): void { for (const receipt of receipts) { this.#lastAcceptedSeq = Math.max(this.#lastAcceptedSeq, receipt.seq); } + + for (const event of events) { + if (event.sourceEventId !== undefined) { + this.#acceptedInFlightSourceEventIds.add(event.sourceEventId); + } + } + + const settlement = this.#terminalSettlement; + + if (settlement === null) { + return; + } + + for (const event of events) { + const sourceEventId = event.sourceEventId; + + if ( + sourceEventId !== undefined && + settlement.events.some((candidate) => candidate.sourceEventId === sourceEventId) + ) { + settlement.acceptedSourceEventIds.add(sourceEventId); + } + } + + this.#advanceTerminalSettlement(settlement); + } + + #advanceTerminalSettlement(settlement: TerminalSettlement): void { + while ( + settlement.acceptedSourceEventIds.delete( + settlement.events[settlement.nextIndex]?.sourceEventId ?? "", + ) + ) { + settlement.nextIndex += 1; + } } } diff --git a/src/runtimes/mcp/remote-http-mcp-executor.ts b/src/runtimes/mcp/remote-http-mcp-executor.ts index f11e607..c35067e 100644 --- a/src/runtimes/mcp/remote-http-mcp-executor.ts +++ b/src/runtimes/mcp/remote-http-mcp-executor.ts @@ -1,10 +1,13 @@ import { Client, + InsufficientScopeError, ProtocolError, ProtocolErrorCode, SdkError, SdkErrorCode, + SdkHttpError, StreamableHTTPClientTransport, + UnauthorizedError, } from "@modelcontextprotocol/client"; import type { AuthProvider, CallToolResult } from "@modelcontextprotocol/client"; @@ -77,21 +80,32 @@ function normalizeCallToolResult( result: CallToolResult, command: McpExecuteCommand, ): McpExternalToolExecutionResult { - const textContent = result.content - .flatMap((block) => (block.type === "text" ? [block.text] : [])) - .map((text) => text.trim()) - .filter((text) => text.length > 0); + const textContent = result.content.flatMap((block) => + block.type === "text" ? [block.text] : [], + ); + const plainText = + result.content.length === 1 && + result.content[0]?.type === "text" && + Object.keys(result.content[0]).every((key) => key === "type" || key === "text"); + const hasRichContent = !plainText || result.structuredContent !== undefined; - const outputText = - textContent.length > 0 + const outputText = hasRichContent + ? JSON.stringify( + { + content: result.content, + ...(result.isError === undefined ? {} : { isError: result.isError }), + ...(result.structuredContent === undefined + ? {} + : { structuredContent: result.structuredContent }), + }, + null, + 2, + ) + : textContent.length > 0 ? textContent.join("\n\n") - : result.structuredContent !== undefined - ? JSON.stringify(result.structuredContent, null, 2) - : result.content.length > 0 - ? JSON.stringify(result.content, null, 2) - : result.isError === true - ? `MCP tool ${command.toolName} reported an error without textual details.` - : ""; + : result.isError === true + ? `MCP tool ${command.toolName} reported an error without textual details.` + : ""; return { ...(result.isError === undefined ? {} : { isError: result.isError }), @@ -108,25 +122,64 @@ function mapMcpExecutionError( server: ActiveMcpServer, error: unknown, ): Error { - if (error instanceof SdkError) { - switch (error.code) { - case SdkErrorCode.ClientHttpAuthentication: { + if (error instanceof SdkHttpError) { + switch (error.status) { + case 400: + return new Error( + `MCP server ${server.name} rejected the request for ${command.toolName} (HTTP 400).`, + ); + case 401: return new Error( `MCP authorization for ${server.name} is no longer valid. Refresh or reconnect the credential and retry.`, ); - } - case SdkErrorCode.ClientHttpForbidden: { + case 403: return new Error( `MCP server ${server.name} rejected the credential for ${command.toolName}. Access may have been revoked.`, ); - } + case 404: + return new Error(`MCP HTTP endpoint for ${server.name} was not found.`); + case 405: + return new Error(`MCP server ${server.name} does not support HTTP tool execution.`); + case 408: + return new Error(`Timed out while calling MCP tool ${command.toolName} on ${server.name}.`); + case 409: + return new Error( + `MCP server ${server.name} reported a conflict while calling ${command.toolName}. Retry the request.`, + ); + case 429: + return new Error( + `MCP server ${server.name} rate limited ${command.toolName}. Retry the request later.`, + ); + default: + return error.status >= 500 + ? new Error( + `MCP server ${server.name} failed while calling ${command.toolName} (HTTP ${error.status}).`, + ) + : new Error( + `MCP server ${server.name} rejected ${command.toolName} with HTTP ${error.status}.`, + ); + } + } + + if (error instanceof UnauthorizedError) { + return new Error( + `MCP authorization for ${server.name} is no longer valid. Refresh or reconnect the credential and retry.`, + ); + } + + if (error instanceof InsufficientScopeError) { + return new Error( + `MCP credential for ${server.name} lacks the access required for ${command.toolName}. Reauthorize the credential and retry.`, + ); + } + + if (error instanceof SdkError) { + switch (error.code) { case SdkErrorCode.RequestTimeout: { return new Error(`Timed out while calling MCP tool ${command.toolName} on ${server.name}.`); } case SdkErrorCode.ConnectionClosed: case SdkErrorCode.SendFailed: - case SdkErrorCode.ClientHttpFailedToOpenStream: - case SdkErrorCode.ClientHttpUnexpectedContent: case SdkErrorCode.NotConnected: { return new Error(`Failed to reach MCP server ${server.name}: ${error.message}`); } @@ -135,12 +188,8 @@ function mapMcpExecutionError( `MCP server ${server.name} does not support the requested capability for ${command.toolName}.`, ); } - case SdkErrorCode.ClientHttpNotImplemented: { - return new Error(`MCP server ${server.name} does not support HTTP tool execution.`); - } case SdkErrorCode.AlreadyConnected: - case SdkErrorCode.NotInitialized: - case SdkErrorCode.ClientHttpFailedToTerminateSession: { + case SdkErrorCode.NotInitialized: { return new Error(`MCP client state error for ${server.name}: ${error.message}`); } default: { @@ -164,20 +213,6 @@ function mapMcpExecutionError( } if (error instanceof Error) { - const lowered = error.message.toLowerCase(); - - if (lowered.includes("unauthorized") || lowered.includes("401")) { - return new Error( - `MCP authorization for ${server.name} is no longer valid. Refresh or reconnect the credential and retry.`, - ); - } - - if (lowered.includes("forbidden") || lowered.includes("403")) { - return new Error( - `MCP server ${server.name} rejected the credential for ${command.toolName}. Access may have been revoked.`, - ); - } - return new Error( `Failed to execute MCP tool ${command.toolName} on ${server.name}: ${error.message}`, ); @@ -198,10 +233,15 @@ export async function executeRemoteHttpMcpCommand( const authProvider: AuthProvider = { token: async () => server.proxyGrantId, }; - const client = new Client({ - name: "mosoo-driver", - version: AGENT_DRIVER_VERSION, - }); + const client = new Client( + { + name: "mosoo-driver", + version: AGENT_DRIVER_VERSION, + }, + { + versionNegotiation: { mode: "auto" }, + }, + ); const transport = new StreamableHTTPClientTransport(new URL(server.proxyUrl), { authProvider, requestInit: { @@ -233,12 +273,10 @@ export async function executeRemoteHttpMcpCommand( } catch (error) { throw mapMcpExecutionError(command, server, error); } finally { - if (!requestSignal.aborted) { - await settlePromiseWithTimeout(transport.terminateSession(), { - label: "MCP session termination", - timeoutMs: MCP_CLEANUP_TIMEOUT_MS, - }); - } + await settlePromiseWithTimeout(transport.terminateSession(), { + label: "MCP session termination", + timeoutMs: MCP_CLEANUP_TIMEOUT_MS, + }); await settlePromiseWithTimeout(client.close(), { label: "MCP client close", diff --git a/src/runtimes/openai/app-server-client.ts b/src/runtimes/openai/app-server-client.ts index 3a1e7c2..820f983 100644 --- a/src/runtimes/openai/app-server-client.ts +++ b/src/runtimes/openai/app-server-client.ts @@ -24,13 +24,7 @@ import { } from "../child-process"; import type { BoundSpawnedProcess } from "../child-process"; import { summarizeOpenAiProxyEnv } from "./app-server-env"; -import { - isRecord, - readNonEmptyString, - readRecord, - readString, - toJsonRpcId, -} from "./app-server-json"; +import { isRecord, readNonEmptyString, toJsonRpcId } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; import { materializeOpenAiApiKeyAuthState, @@ -43,15 +37,18 @@ import type { RequestId, ServerNotificationMethod, ServerNotificationParams, -} from "./generated/app-server-protocol"; +} from "./app-server-protocol"; import { CLIENT_REQUEST_RESULT_PARSERS, isServerNotificationMethod, isServerRequestMethod, - parseServerNotificationParams, -} from "./generated/app-server-protocol"; + parseServerNotification, + parseServerRequest, +} from "./app-server-protocol"; import { buildOpenAiMcpServerConfig } from "./mcp-config"; import { OpenAiAppServerRequestHandler } from "./app-server-request-handler"; +import { jsonRpcResponseSchema } from "./app-server-protocol-client-schemas"; +import { toOpenAiProtocolError } from "./app-server-event-mapping"; interface PendingJsonRpcRequest { method: string; @@ -74,6 +71,7 @@ interface OpenAiClientContext extends AgentDriverContext { params: ServerNotificationParams[M], ): Promise; handleProtocolError(error: Error): Promise; + mapToolCallId(toolCallId: string): string; } function summarizeJsonRpcErrorData(value: unknown): JsonObject | null { @@ -217,6 +215,7 @@ export class OpenAiAppServerClient { context, handleError: async (error) => this.#failProtocol(error), isStopped: () => this.#stopRequested, + mapToolCallId: context.mapToolCallId, respond: (id, result) => this.respond(id, result), respondError: (id, message) => this.respondError(id, message), }); @@ -478,7 +477,7 @@ export class OpenAiAppServerClient { return; } - this.notify("initialized", {}); + this.notify("initialized", undefined); }); return { phases }; @@ -493,16 +492,7 @@ export class OpenAiAppServerClient { } async cleanBackgroundTerminals(threadId: string, signal?: AbortSignal): Promise { - await this.#request( - "thread/backgroundTerminals/clean", - { threadId }, - (value) => { - if (!isRecord(value ?? {})) { - throw new Error("thread/backgroundTerminals/clean result must be an object."); - } - }, - signal, - ); + await this.request("thread/backgroundTerminals/clean", { threadId }, signal); } async #request( @@ -730,19 +720,28 @@ export class OpenAiAppServerClient { try { parsed = JSON.parse(trimmed); } catch { - this.#context.logger.debug("driver.openai.non_json_stdout", { - line: trimmed, - }); + this.#failProtocol(new TypeError("OpenAi app-server stdout is not valid JSON.")); return; } if (!isRecord(parsed)) { + this.#failProtocol(new TypeError("OpenAi app-server protocol message must be an object.")); return; } const method = readNonEmptyString(parsed, "method"); const id = toJsonRpcId(parsed["id"]); + if ("result" in parsed || "error" in parsed) { + if (id === null) { + this.#failProtocol(new TypeError("OpenAi app-server response requires a valid id.")); + return; + } + + this.#onResponse(id, parsed); + return; + } + if (method !== null) { const bytes = Buffer.byteLength(trimmed, "utf8"); @@ -756,9 +755,9 @@ export class OpenAiAppServerClient { this.#pauseServerMessagesIfNeeded(); this.#serverMessageQueue = this.#processMessage( this.#serverMessageQueue, + parsed, method, id, - parsed["params"], ).finally(() => { this.#pendingServerMessages -= 1; this.#pendingServerMessageBytes -= bytes; @@ -769,7 +768,12 @@ export class OpenAiAppServerClient { if (id !== null) { this.#onResponse(id, parsed); + return; } + + this.#failProtocol( + new TypeError("OpenAi app-server protocol message requires a valid method or id."), + ); } #pauseServerMessagesIfNeeded(): void { @@ -840,9 +844,9 @@ export class OpenAiAppServerClient { async #processMessage( previousMessage: Promise, + message: JsonObject, method: string, id: RequestId | null, - params: unknown, ): Promise { try { await previousMessage; @@ -851,20 +855,26 @@ export class OpenAiAppServerClient { return; } - await this.#onServerMessage(method, id, params); + await this.#onServerMessage(message, method, id); } catch (error) { + const failure = + error instanceof Error ? error : new Error("OpenAi app-server protocol message failed."); this.#context.logger.error("driver.openai.server_message.failed", error, { method, }); + + if (id === null) { + this.#failProtocol(failure); + return; + } + if (!this.#stopRequested) { - await this.#notifyProtocolError( - error instanceof Error ? error : new Error("OpenAi app-server protocol message failed."), - ); + await this.#notifyProtocolError(failure); } if (id !== null && !this.#stopRequested) { try { - this.respondError(id, error instanceof Error ? error.message : "Server request failed."); + this.respondError(id, failure.message); } catch (responseError) { this.#context.logger.error("driver.openai.server_error_response.failed", responseError, { method, @@ -875,6 +885,17 @@ export class OpenAiAppServerClient { } #onResponse(id: RequestId, message: JsonObject): void { + const parsed = jsonRpcResponseSchema.safeParse(message); + + if (!parsed.success) { + this.#failProtocol( + new TypeError("OpenAi app-server response envelope is invalid.", { + cause: parsed.error, + }), + ); + return; + } + const pending = this.#pendingRequests.get(id); if (pending === undefined) { @@ -883,37 +904,41 @@ export class OpenAiAppServerClient { this.#pendingRequests.delete(id); - const responseError = readRecord(message, "error"); + if ("error" in parsed.data) { + const { error } = parsed.data; - if (responseError !== null) { - const errorMessage = - readString(responseError, "message") ?? "OpenAi app-server request failed."; - const responseCode = responseError["code"]; - const errorCode = - typeof responseCode === "number" || typeof responseCode === "string" ? responseCode : null; - - this.#context.logger.error("driver.openai.client_request.failed", new Error(errorMessage), { - data: summarizeJsonRpcErrorData(responseError["data"]), - method: pending.method, - responseCode: errorCode, - }); - pending.reject(new Error(errorMessage)); + this.#context.logger.error( + "driver.openai.client_request.failed", + new Error(toOpenAiProtocolError({ message: error.message }).message), + { + data: summarizeJsonRpcErrorData(error.data), + method: pending.method, + responseCode: error.code, + }, + ); + pending.reject(new Error(error.message)); return; } try { - pending.resolve(message["result"]); + pending.resolve(parsed.data.result); } catch (parseError) { - pending.reject( + const error = parseError instanceof Error ? parseError - : new Error("OpenAi app-server result parse failed."), - ); + : new Error("OpenAi app-server result parse failed."); + pending.reject(error); + this.#failProtocol(error); } } - async #onServerMessage(method: string, id: RequestId | null, params: unknown): Promise { + async #onServerMessage(message: JsonObject, method: string, id: RequestId | null): Promise { if (id === null) { + if (isServerRequestMethod(method)) { + parseServerRequest(message); + throw new Error(`OpenAi app-server request ${method} is missing a valid id.`); + } + if (!isServerNotificationMethod(method)) { this.#context.logger.debug("driver.openai.server_notification.ignored", { method, @@ -921,14 +946,21 @@ export class OpenAiAppServerClient { return; } - if (method === "serverRequest/resolved") { - const parsed = parseServerNotificationParams(method, params); - await this.#requestHandler.resolveElsewhere(parsed.requestId); - await this.#context.handleNotification(method, parsed); + const notification = parseServerNotification(message)!; + + if (notification.method === "serverRequest/resolved") { + const requestId = notification.params["requestId"]; + + if (typeof requestId !== "number" && typeof requestId !== "string") { + throw new TypeError("serverRequest/resolved params.requestId is invalid."); + } + + await this.#requestHandler.resolveElsewhere(requestId); + await this.#context.handleNotification(notification.method, notification.params); return; } - await this.#context.handleNotification(method, parseServerNotificationParams(method, params)); + await this.#context.handleNotification(notification.method, notification.params); return; } @@ -937,7 +969,16 @@ export class OpenAiAppServerClient { return; } - this.#requestHandler.dispatch(method, id, params); + const request = parseServerRequest(message)!; + + if (this.#requestHandler.isPending(request.id)) { + this.#failProtocol( + new Error(`OpenAi app-server request ${String(request.id)} is already pending.`), + ); + return; + } + + this.#requestHandler.dispatch(request.method, request.id, request.params); } #rejectPending(error: Error): void { diff --git a/src/runtimes/openai/app-server-driver-backend.ts b/src/runtimes/openai/app-server-driver-backend.ts index ece6e36..62f9907 100644 --- a/src/runtimes/openai/app-server-driver-backend.ts +++ b/src/runtimes/openai/app-server-driver-backend.ts @@ -1,6 +1,9 @@ import { isDriverFullAccess } from "../../core/driver-permission-policy"; import { pushLosslessEvents } from "../../core/driver-runtime-io"; -import { DriverTurnCancelledError } from "../../core/driver-runtime-state"; +import { + DriverTurnCancelledError, + DriverTurnCancellationCleanupError, +} from "../../core/driver-runtime-state"; import { createTimingEvent, createTimingPhase, @@ -11,7 +14,7 @@ import type { RunId } from "../../protocol/id"; import type { DriverRuntime } from "../../protocol/runtime"; import type { DriverStartInput } from "../../protocol/start"; import type { RuntimeCommandInput } from "../../runtime-command"; -import { raceWithAbort } from "../../utils/async"; +import { raceWithAbort, settlePromiseWithTimeout } from "../../utils/async"; import type { AgentDriverBackend, AgentDriverContext } from "../../core/agent-driver-backend"; import { DriverEventPublisher } from "../driver-event-publisher"; import { @@ -22,16 +25,17 @@ import { import { OpenAiAppServerClient } from "./app-server-client"; import { MOSOO_OPENAI_RUNTIME_SANDBOX_MODE } from "./app-server-env"; import { OpenAiAppServerEventBridge } from "./app-server-event-bridge"; +import { toOpenAiProtocolError } from "./app-server-event-mapping"; import type { ApprovalPolicy, - JsonObject, + ThreadInjectItemsParams, ThreadResumeParams, ThreadStartParams, ThreadStartResponse, TurnStatus, TurnStartParams, TurnStartResponse, -} from "./generated/app-server-protocol"; +} from "./app-server-protocol"; /** * App-server approval policy derived from the driver permission policy. @@ -63,6 +67,24 @@ const OPENAI_CLIENT_STOP_TIMEOUT_MS = 500; const OPENAI_SERVER_REQUEST_CANCEL_TIMEOUT_MS = 1_500; const OPENAI_TURN_CANCEL_EVENT_TIMEOUT_MS = 250; const OPENAI_BACKGROUND_TERMINAL_CLEAN_TIMEOUT_MS = 500; +const MAX_OPENAI_NATIVE_THREAD_ID_BYTES = 256; + +function validateOpenAiNativeThreadId(threadId: string): string { + const utf8Bytes = Buffer.byteLength(threadId, "utf8"); + + if (utf8Bytes === 0 || utf8Bytes > MAX_OPENAI_NATIVE_THREAD_ID_BYTES) { + throw new RangeError( + `OpenAI native thread ID must contain 1-${String(MAX_OPENAI_NATIVE_THREAD_ID_BYTES)} UTF-8 bytes (received ${String(utf8Bytes)}).`, + ); + } + + return threadId; +} + +function validateOpenAiThreadResponse(response: ThreadStartResponse): ThreadStartResponse { + validateOpenAiNativeThreadId(response.thread.id); + return response; +} async function joinCancellationCleanup(tasks: readonly Promise[]): Promise { const results = await Promise.allSettled(tasks); @@ -87,11 +109,7 @@ function readResumeThreadId(payload: DriverStartInput): string | null { throw new Error("OpenAI runtime received an incompatible native resume ref."); } - if (nativeResumeRef.value.length === 0) { - throw new Error("OpenAI runtime received an empty native resume thread id."); - } - - return nativeResumeRef.value; + return validateOpenAiNativeThreadId(nativeResumeRef.value); } function isTerminalTurn(status: TurnStatus): boolean { @@ -111,7 +129,7 @@ function isUnmaterializedRollout(error: unknown, threadId: string): boolean { function toRecoveryItems( messages: DriverStartInput["execution"]["session"]["recoveryMessages"], -): JsonObject[] { +): ThreadInjectItemsParams["items"] { return messages.map((message) => ({ content: [ { @@ -124,7 +142,7 @@ function toRecoveryItems( })); } -export function createTurnParams(input: OpenAiTurnStartInput): TurnStartParams { +function createTurnParams(input: OpenAiTurnStartInput): TurnStartParams { return { approvalPolicy: input.approvalPolicy, cwd: input.cwd, @@ -151,6 +169,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { #pendingTurnStartCancellationEvent = false; #pendingTurnStartCancellationReason: string | null = null; #pendingTurnStartServerRequests: Promise | null = null; + #pendingTurnStartUpdates: Promise | null = null; #restartThreadId: string | null = null; #stopping = false; #threadId: string | null = null; @@ -172,6 +191,10 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { } }, push: async (context, reason, events) => this.#eventPublisher.push(context, reason, events), + pushSession: async (context, reason, events) => + this.#eventPublisher.pushSession(context, reason, events), + pushTerminal: async (context, reason, closures, terminal) => + this.#eventPublisher.pushTerminal(context, reason, closures, terminal), requireThreadId: () => this.#requireThreadId(), }); @@ -267,6 +290,8 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { handleNotification: async (method, params) => this.#events.handleNotification(context, method, params), handleProtocolError: async (error) => { + const boundedError = new Error(toOpenAiProtocolError({ message: error.message }).message); + try { if (await this.#events.failActiveTurns(context, error)) { return; @@ -275,13 +300,14 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { return; } } catch (projectionError) { - this.#events.rejectActiveTurns(error); - context.lifecycle.fail(error); + this.#events.rejectActiveTurns(boundedError); + context.lifecycle.fail(boundedError); throw projectionError; } - context.lifecycle.fail(error); + context.lifecycle.fail(boundedError); }, + mapToolCallId: (toolCallId) => this.#events.mapToolCallId(toolCallId), }); } @@ -328,21 +354,25 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { } satisfies ThreadStartParams; if (resumeThreadId === null) { - return measure("thread.start", () => - client.request("thread/start", threadStartParams, signal), + return validateOpenAiThreadResponse( + await measure("thread.start", () => + client.request("thread/start", threadStartParams, signal), + ), ); } try { - return await measure("thread.resume", () => - client.request( - "thread/resume", - { - ...baseThreadParams, - ...(developerInstructions === null ? {} : { developerInstructions }), - threadId: resumeThreadId, - } satisfies ThreadResumeParams, - signal, + return validateOpenAiThreadResponse( + await measure("thread.resume", () => + client.request( + "thread/resume", + { + ...baseThreadParams, + ...(developerInstructions === null ? {} : { developerInstructions }), + threadId: resumeThreadId, + } satisfies ThreadResumeParams, + signal, + ), ), ); } catch (error) { @@ -354,8 +384,10 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { context.logger.warn("driver.openai.native_resume_ref.missing_rollout", { nativeResumeRefPresent: true, }); - const threadResult = await measure("thread.start_after_missing_rollout", () => - client.request("thread/start", threadStartParams, signal), + const threadResult = validateOpenAiThreadResponse( + await measure("thread.start_after_missing_rollout", () => + client.request("thread/start", threadStartParams, signal), + ), ); const recoveryItems = toRecoveryItems(this.#payload.execution.session.recoveryMessages); @@ -467,8 +499,9 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { this.#pendingTurnStartCancellationEvent = false; this.#pendingTurnStartCancellationReason = null; this.#pendingTurnStartServerRequests = null; + this.#pendingTurnStartUpdates = null; - let client: OpenAiAppServerClient; + let client: OpenAiAppServerClient | null = null; let threadId: string; let turnResult: TurnStartResponse; let turnStartRequestedAtMs: number; @@ -499,46 +532,62 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { }), ); } catch (error) { - this.#turnStartInFlight = false; - this.#turnStartRunId = null; const publishCancellation = this.#pendingTurnStartCancellationEvent; const pendingCancellationReason = this.#pendingTurnStartCancellationReason; - this.#pendingTurnStartCancellationEvent = false; - this.#pendingTurnStartCancellationReason = null; + const failure = error instanceof Error ? error : new Error("OpenAI turn start failed."); if (pendingCancellationReason !== null) { - await this.#waitPendingTurnStartCleanup(); - await this.#waitPendingTurnStartServerRequests(); - if (publishCancellation) { - await this.#publishTurnStartCancellation(context, runId, pendingCancellationReason); - } + await this.#finishPendingTurnStartCancellation( + context, + runId, + pendingCancellationReason, + publishCancellation, + ); + this.#events.releaseTurnState(); + this.#turnStartInFlight = false; + this.#turnStartRunId = null; + this.#pendingTurnStartCancellationEvent = false; + this.#pendingTurnStartCancellationReason = null; throw new DriverTurnCancelledError(pendingCancellationReason); } - throw error; + try { + await this.#failTurnStart(context, failure); + } finally { + this.#turnStartInFlight = false; + this.#turnStartRunId = null; + } + throw new Error(toOpenAiProtocolError({ message: failure.message }).message); } const turnId = turnResult.turn.id; + const publicTurnId = this.#events.publicTurnId(turnId); const completion = this.#events.trackTurn(turnId, runId); void completion.catch(() => {}); - this.#turnStartInFlight = false; - this.#turnStartRunId = null; const publishCancellation = this.#pendingTurnStartCancellationEvent; - this.#pendingTurnStartCancellationEvent = false; const pendingCancellationReason = this.#pendingTurnStartCancellationReason; - this.#pendingTurnStartCancellationReason = null; if (pendingCancellationReason !== null) { + await this.#finishPendingTurnStartCancellation( + context, + runId, + pendingCancellationReason, + publishCancellation, + ); this.#events.rejectTurn(turnId, new DriverTurnCancelledError(pendingCancellationReason)); - await this.#waitPendingTurnStartCleanup(); - await this.#waitPendingTurnStartServerRequests(); - if (publishCancellation) { - await this.#publishTurnStartCancellation(context, runId, pendingCancellationReason); - } this.#events.releaseTurnState(); + this.#turnStartInFlight = false; + this.#turnStartRunId = null; + this.#pendingTurnStartCancellationEvent = false; + this.#pendingTurnStartCancellationReason = null; throw new DriverTurnCancelledError(pendingCancellationReason); } + this.#turnStartInFlight = false; + this.#turnStartRunId = null; + this.#pendingTurnStartCancellationEvent = false; + this.#pendingTurnStartCancellationReason = null; + const turnStartedAtMs = Date.now(); await this.#eventPublisher.push(context, "driver.openai.provider.turn_start", [ @@ -553,17 +602,19 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { ], runId, sessionId: context.payload.execution.run.sessionId, - sourceEventId: `openai.provider.turn_start:${turnId}`, + sourceEventId: `openai.provider.turn_start:${publicTurnId}`, stage: "driver_turn", startedAt: new Date(turnStartRequestedAtMs).toISOString(), native: { eventName: "provider.turn_start", provider: "openai", - turnId, + turnId: publicTurnId, }, }), ]); + await this.#events.publishRunStarted(context, { runId, turnId }); + if (isTerminalTurn(turnResult.turn.status)) { await this.#events.handleNotification(context, "turn/completed", { threadId, @@ -571,7 +622,6 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { }); } - await this.#events.publishRunStarted(context, { runId, turnId }); await completion; } @@ -580,28 +630,34 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { if (!this.#turnStartInFlight || runId === null) { return false; } - this.#turnStartRunId = null; - await this.#eventPublisher.push(context, "driver.openai.provider.failed", [ - { - kind: "run.started", - payload: { - startedAt: new Date().toISOString(), + + await this.#eventPublisher.pushTerminal( + context, + "driver.openai.provider.failed", + [ + { + kind: "run.started", + payload: { + startedAt: new Date().toISOString(), + }, + runId, + sourceEventId: `openai.provider.failed.started:${runId}`, }, - runId, - }, + ], { kind: "run.failed", payload: { error: { + ...toOpenAiProtocolError({ message: error.message }), code: "openai.provider_failed", - message: error.message, }, recoverable: false, }, runId, + sourceEventId: `openai.provider.failed.terminal:${runId}`, }, - ]); + ); return true; } @@ -610,7 +666,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { runId: RunId, reason: string, ): Promise { - const cancellationEvent = this.#eventPublisher.push( + await this.#eventPublisher.pushTerminal( context, "driver.openai.turn_start.cancelled", [ @@ -620,6 +676,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { startedAt: new Date().toISOString(), }, runId, + sourceEventId: `openai.turn_start.cancelled.started:${runId}`, }, { kind: "run.cancel.requested", @@ -629,29 +686,47 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { targetRunId: runId, }, runId, + sourceEventId: `openai.turn_start.cancelled.requested:${runId}`, }, - { - kind: "run.cancelled", - payload: { - requestedBy: "user", - stopReason: "cancelled", - }, + ...this.#events.turnStartCancellationEvents().map((event, index) => ({ + ...event, runId, - }, + sourceEventId: + event.sourceEventId ?? `openai.turn_start.cancelled.closure:${runId}:${String(index)}`, + })), ], + { + kind: "run.cancelled", + payload: { + requestedBy: "user", + stopReason: "cancelled", + }, + runId, + sourceEventId: `openai.turn_start.cancelled.terminal:${runId}`, + }, ); - void cancellationEvent.catch(() => {}); + } + async #finishPendingTurnStartCancellation( + context: AgentDriverContext, + runId: RunId, + reason: string, + publish: boolean, + ): Promise { try { - await raceWithAbort( - cancellationEvent, - AbortSignal.timeout(OPENAI_TURN_CANCEL_EVENT_TIMEOUT_MS), - ); + await this.#waitPendingTurnStartCleanup(); + await this.#waitPendingTurnStartServerRequests(); + await this.#waitPendingTurnStartUpdates(); + if (publish) { + await this.#publishTurnStartCancellation(context, runId, reason); + } } catch (error) { - context.logger.warn("driver.openai.turn_start.cancellation_event.failed", { - message: error instanceof Error ? error.message : "cancellation event failed", - reason, - }); + throw new DriverTurnCancellationCleanupError( + `OpenAI pending turn cancellation cleanup failed: ${ + error instanceof Error ? error.message : "unknown cleanup error" + }`, + error, + ); } } @@ -671,8 +746,29 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { } } + async #waitPendingTurnStartUpdates(): Promise { + const pending = this.#pendingTurnStartUpdates; + this.#pendingTurnStartUpdates = null; + if (pending !== null) { + await pending; + } + } + async cancelActiveTurn(context: AgentDriverContext, reason: string): Promise { - await this.#cancelActiveTurn(context, reason, true); + try { + await this.#cancelActiveTurn(context, reason, true); + } catch (error) { + if (error instanceof DriverTurnCancellationCleanupError) { + throw error; + } + + throw new DriverTurnCancellationCleanupError( + `OpenAI cancelled turn cleanup failed: ${ + error instanceof Error ? error.message : "unknown cleanup error" + }`, + error, + ); + } } async #cancelActiveTurn( @@ -700,8 +796,9 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { this.#pendingTurnStartServerRequests = serverRequests; const cleanup = this.#closeClientForCancellation(context, client, threadId, reason); this.#pendingTurnStartCleanup = cleanup; + this.#pendingTurnStartUpdates = cleanup.then(() => client.drainServerMessages()); + void this.#pendingTurnStartUpdates.catch(() => {}); await cleanup; - this.#events.releaseTurnState(); return; } @@ -740,28 +837,46 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { this.#closeClientForCancellation(context, client, threadId, reason), ]); } catch (error) { - this.#events.rejectActiveTurns( - error instanceof Error ? error : new Error("OpenAI cancellation cleanup failed."), + const cleanupError = new DriverTurnCancellationCleanupError( + `OpenAI cancelled turn cleanup failed: ${ + error instanceof Error ? error.message : "unknown cleanup error" + }`, + error, ); - throw error; + this.#events.rejectActiveTurns(cleanupError); + throw cleanupError; } + const updatesDrained = client.drainServerMessages(); const cancellationEvents = Promise.all( - activeTurnIds.map((turnId) => this.#events.cancelTurn(context, turnId, reason)), - ); + activeTurnIds.map((turnId) => + this.#events.cancelTurn(context, turnId, reason, () => updatesDrained), + ), + ).catch((error: unknown) => { + const cleanupError = new DriverTurnCancellationCleanupError( + `OpenAI cancellation event delivery failed: ${ + error instanceof Error ? error.message : "unknown delivery error" + }`, + error, + ); + this.#events.rejectActiveTurns(cleanupError); + throw cleanupError; + }); void cancellationEvents.catch(() => {}); + const cancellationSettlement = await settlePromiseWithTimeout(cancellationEvents, { + label: "OpenAI turn cancellation event delivery", + timeoutMs: OPENAI_TURN_CANCEL_EVENT_TIMEOUT_MS, + }); - try { - await raceWithAbort( - cancellationEvents, - AbortSignal.timeout(OPENAI_TURN_CANCEL_EVENT_TIMEOUT_MS), - ); - } catch (error) { - context.logger.warn("driver.openai.turn.cancellation_event.failed", { - message: error instanceof Error ? error.message : "cancellation event failed", + if (cancellationSettlement.status === "failed") { + throw cancellationSettlement.error; + } + + if (cancellationSettlement.status === "timed_out") { + context.logger.warn("driver.openai.turn.cancellation_event.pending", { + message: cancellationSettlement.error.message, reason, }); } - this.#events.releaseTurnState(); } #boundServerRequestCancellation( @@ -801,9 +916,6 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { } } catch (error) { context.logger.error("driver.openai.cancel.client_stop.failed", error, { reason }); - this.#events.rejectActiveTurns( - error instanceof Error ? error : new Error("OpenAI app-server cleanup failed."), - ); throw error; } } diff --git a/src/runtimes/openai/app-server-event-bridge.ts b/src/runtimes/openai/app-server-event-bridge.ts index 0cf972b..11e27ff 100644 --- a/src/runtimes/openai/app-server-event-bridge.ts +++ b/src/runtimes/openai/app-server-event-bridge.ts @@ -2,33 +2,122 @@ import { DriverTurnCancelledError } from "../../core/driver-runtime-state"; import type { DriverEventInput } from "../../protocol/events"; import type { RunId } from "../../protocol/id"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; -import { toOpenAiErrorMessage, toOpenAiSessionUsageSummary } from "./app-server-event-mapping"; +import { toOpenAiProtocolError } from "./app-server-event-mapping"; import { OpenAiItemState, OpenAiMessageState, OpenAiPlanState, + OpenAiPublicIdState, + OpenAiSessionUsageState, OpenAiToolState, } from "./app-server-event-state"; -import { OpenAiAppServerItemEventBridge } from "./app-server-item-events"; -import { isRecord, readNonEmptyString, readRecord, readString } from "./app-server-json"; +import { chunkOpenAiText, OpenAiAppServerItemEventBridge } from "./app-server-item-events"; +import { isRecord, readArray, readNonEmptyString, readRecord, readString } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; import { OpenAiTurnTracker } from "./app-server-turn-tracker"; -import type { - ServerNotificationMethod, - ServerNotificationParams, -} from "./generated/app-server-protocol"; +import type { ServerNotificationMethod, ServerNotificationParams } from "./app-server-protocol"; interface OpenAiAppServerEventBridgeOptions { beforeInterruptedTurn?(context: AgentDriverContext, turnId: string): Promise; push(context: AgentDriverContext, reason: string, events: DriverEventInput[]): Promise; + pushSession( + context: AgentDriverContext, + reason: string, + events: DriverEventInput[], + ): Promise; + pushTerminal( + context: AgentDriverContext, + reason: string, + closures: readonly DriverEventInput[], + terminal: DriverEventInput, + ): Promise; requireThreadId(): string; } -function turnEventId(eventName: string, turnId: string): string { - return `openai.${eventName}:${turnId}`; +const MAX_OPENAI_TELEMETRY_FIELD_BYTES = 4 * 1_024; +const MAX_WORLD_WRITABLE_SAMPLE_PATHS = 16; + +function jsonUtf8Bytes(value: unknown): number { + return Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8"); } -function turnEventFields(input: { eventName: string; runId?: RunId | undefined; turnId: string }): { +function toBoundedOpenAiTelemetry(payload: JsonObject, fields: readonly string[]): JsonObject { + const summary: JsonObject = { utf8Bytes: jsonUtf8Bytes(payload) }; + + for (const field of fields) { + const value = payload[field]; + + if (typeof value === "string") { + const utf8Bytes = Buffer.byteLength(value, "utf8"); + if (utf8Bytes <= MAX_OPENAI_TELEMETRY_FIELD_BYTES) { + summary[field] = value; + } else { + summary[`${field}Utf8Bytes`] = utf8Bytes; + } + continue; + } + + if (Array.isArray(value)) { + const utf8Bytes = jsonUtf8Bytes(value); + if (utf8Bytes <= MAX_OPENAI_TELEMETRY_FIELD_BYTES) { + summary[field] = value; + } else { + summary[`${field}Count`] = value.length; + summary[`${field}Utf8Bytes`] = utf8Bytes; + } + continue; + } + + if (isRecord(value)) { + const utf8Bytes = jsonUtf8Bytes(value); + if (utf8Bytes <= MAX_OPENAI_TELEMETRY_FIELD_BYTES) { + summary[field] = value; + } else { + summary[`${field}Utf8Bytes`] = utf8Bytes; + } + continue; + } + + if ( + value === null || + typeof value === "boolean" || + (typeof value === "number" && Number.isFinite(value)) + ) { + summary[field] = value; + } + } + + return summary; +} + +function toOpenAiWarningEvents( + method: "guardianWarning" | "warning", + message: string, +): DriverEventInput[] { + const chunks = chunkOpenAiText(message); + + return chunks.map((content, index) => ({ + delivery: "lossless", + kind: "message.added", + payload: { + ...(chunks.length === 1 ? {} : { chunkCount: chunks.length, chunkIndex: index }), + content, + level: "warning", + role: "agent", + subtype: method === "guardianWarning" ? "guardian_warning" : "warning", + }, + })); +} + +function turnEventId(eventName: string, publicTurnId: string): string { + return `openai.${eventName}:${publicTurnId}`; +} + +function turnEventFields(input: { + eventName: string; + publicTurnId: string; + runId?: RunId | undefined; +}): { native: { eventName: string; provider: string; turnId: string }; runId?: RunId | undefined; sourceEventId: string; @@ -37,21 +126,39 @@ function turnEventFields(input: { eventName: string; runId?: RunId | undefined; native: { eventName: input.eventName, provider: "openai", - turnId: input.turnId, + turnId: input.publicTurnId, }, ...(input.runId === undefined ? {} : { runId: input.runId }), - sourceEventId: turnEventId(input.eventName, input.turnId), + sourceEventId: turnEventId(input.eventName, input.publicTurnId), }; } +function turnClosureEvents(input: { + eventName: string; + events: readonly DriverEventInput[]; + publicTurnId: string; + runId?: RunId | undefined; +}): DriverEventInput[] { + return input.events.map((event, index) => ({ + ...event, + ...(event.runId === undefined && input.runId !== undefined ? { runId: input.runId } : {}), + sourceEventId: + event.sourceEventId ?? + `openai.${input.eventName}.closure:${input.publicTurnId}:${String(index)}`, + })); +} + export class OpenAiAppServerEventBridge { readonly #itemEvents: OpenAiAppServerItemEventBridge; readonly #items = new OpenAiItemState(); readonly #messages = new OpenAiMessageState(); readonly #options: OpenAiAppServerEventBridgeOptions; readonly #plans = new OpenAiPlanState(); + readonly #publicTurnIds = new OpenAiPublicIdState(); readonly #tools = new OpenAiToolState(); + readonly #turnStarts = new Map>(); readonly #turns = new OpenAiTurnTracker(); + readonly #usage = new OpenAiSessionUsageState(); constructor(options: OpenAiAppServerEventBridgeOptions) { this.#options = options; @@ -60,6 +167,7 @@ export class OpenAiAppServerEventBridge { messages: this.#messages, plans: this.#plans, push: (context, reason, events) => this.#push(context, reason, events), + pushSession: options.pushSession, tools: this.#tools, }); } @@ -68,9 +176,20 @@ export class OpenAiAppServerEventBridge { return this.#turns.activeTurnIds(); } + mapToolCallId(toolCallId: string): string { + return this.#items.publicId(toolCallId); + } + + publicTurnId(turnId: string): string { + return this.#items.publicId(turnId, "turn"); + } + clearActiveTurns(): void { + this.#turnStarts.clear(); + this.#publicTurnIds.reset(); this.#turns.clearActiveTurns(); this.#itemEvents.reset(); + this.#usage.reset(); } async cancelTurn( @@ -79,45 +198,67 @@ export class OpenAiAppServerEventBridge { reason: string, drainUpdates?: () => Promise, ): Promise { + await drainUpdates?.(); const runId = this.#turns.activeRunId(turnId); if (runId === null) { return; } - if (!this.#turns.rejectTurn(turnId, new DriverTurnCancelledError(reason))) { + if (!this.#turns.beginSettlement(turnId)) { return; } - await drainUpdates?.(); - await this.#push(context, "driver.openai.turn.cancelled", [ - { - ...turnEventFields({ eventName: "turn.cancel.requested", runId, turnId }), - kind: "run.cancel.requested", - payload: { - reason, - requestedBy: "user", - targetRunId: runId, - }, - }, - ...this.#itemEvents.finishOpen(), - { - ...turnEventFields({ eventName: "turn.cancelled", runId, turnId }), - kind: "run.cancelled", - payload: { - requestedBy: "user", - stopReason: "cancelled", + const publicTurnId = this.publicTurnId(turnId); + + try { + await this.#options.pushTerminal( + context, + "driver.openai.turn.cancelled", + turnClosureEvents({ + eventName: "turn.cancelled", + events: [ + { + ...turnEventFields({ eventName: "turn.cancel.requested", publicTurnId, runId }), + kind: "run.cancel.requested", + payload: { + reason, + requestedBy: "user", + targetRunId: runId, + }, + }, + ...this.#itemEvents.terminalEvents({ kind: "cancelled" }), + ], + publicTurnId, + runId, + }), + { + ...turnEventFields({ eventName: "turn.cancelled", publicTurnId, runId }), + kind: "run.cancelled", + payload: { + requestedBy: "user", + stopReason: "cancelled", + }, }, - }, - ]); + ); + this.#finishSettlement(turnId, { + error: new DriverTurnCancelledError(reason), + kind: "failed", + }); + } catch (pushError) { + this.#turns.cancelSettlement(turnId); + throw pushError; + } } rejectTurn(turnId: string, error: Error): void { this.#turns.rejectTurn(turnId, error); + this.#usage.release(turnId); } rejectActiveTurns(error: Error): void { this.#turns.rejectActiveTurns(error); this.#itemEvents.reset(); + this.#usage.reset(); } async failActiveTurns(context: AgentDriverContext, error: Error): Promise { @@ -130,21 +271,30 @@ export class OpenAiAppServerEventBridge { } try { - await this.#push(context, "driver.openai.provider.failed", [ - ...this.#itemEvents.finishOpen(), + const publicTurnId = this.publicTurnId(turnId); + const protocolError = { + ...toOpenAiProtocolError({ message: error.message }), + code: "openai.provider_failed", + }; + await this.#options.pushTerminal( + context, + "driver.openai.provider.failed", + turnClosureEvents({ + eventName: "provider.failed", + events: this.#itemEvents.terminalEvents({ error: protocolError, kind: "failed" }), + publicTurnId, + runId, + }), { - ...turnEventFields({ eventName: "provider.failed", runId, turnId }), + ...turnEventFields({ eventName: "provider.failed", publicTurnId, runId }), kind: "run.failed", payload: { - error: { - code: "openai.provider_failed", - message: error.message, - }, + error: protocolError, recoverable: false, }, }, - ]); - this.#finishSettlement(turnId, { error, kind: "failed" }); + ); + this.#finishSettlement(turnId, { error: new Error(protocolError.message), kind: "failed" }); failed = true; } catch (pushError) { this.#turns.cancelSettlement(turnId); @@ -159,6 +309,10 @@ export class OpenAiAppServerEventBridge { this.#itemEvents.reset(); } + turnStartCancellationEvents(): DriverEventInput[] { + return this.#itemEvents.terminalEvents({ kind: "cancelled" }); + } + async trackTurn(turnId: string, runId: RunId): Promise { return this.#turns.track(turnId, runId); } @@ -172,6 +326,7 @@ export class OpenAiAppServerEventBridge { const turnId = readNonEmptyString(payload, "turnId") ?? readNonEmptyString(readRecord(payload, "turn"), "id"); + const notificationMethod: ServerNotificationMethod = method; // Native Codex subagents own child threads and turns. Their direct message // and lifecycle frames are provider-internal activity, not authoritative @@ -185,17 +340,27 @@ export class OpenAiAppServerEventBridge { return; } + const item = readRecord(payload, "item"); + const postTerminalSubAgentActivity = + notificationMethod === "item/completed" && + readString(item, "type") === "subAgentActivity" && + (readString(item, "kind") === "completed" || readString(item, "kind") === "interrupted"); + if (turnId !== null && this.#turns.hasTerminal(turnId)) { + if (postTerminalSubAgentActivity) { + await this.#itemEvents.onPostTerminalSubAgentActivity(context, payload); + } return; } - switch (method) { + switch (notificationMethod) { case "configWarning": { this.#onConfigWarning(context, payload); return; } - case "warning": { - this.#onWarning(context, payload); + case "warning": + case "guardianWarning": { + await this.#onWarning(context, payload, notificationMethod); return; } case "remoteControl/status/changed": { @@ -218,10 +383,37 @@ export class OpenAiAppServerEventBridge { await this.#onTurnStarted(context, payload); return; } + case "hook/started": + case "hook/completed": { + await this.#onHook(context, payload, notificationMethod); + return; + } case "item/started": { await this.#itemEvents.onItemStarted(context, payload); return; } + case "item/autoApprovalReview/started": + case "item/autoApprovalReview/completed": { + await this.#onAutoApprovalReview(context, payload, notificationMethod); + return; + } + case "autoApprovalReview/strictReviewRequired": { + await this.#push(context, "driver.openai.autoApprovalReview.strictReviewRequired", [ + { + delivery: "lossless", + kind: "message.added", + payload: { + ...toBoundedOpenAiTelemetry(payload, ["startedAtMs"]), + content: + "This request requires additional safety checks; tool calls may take longer.", + level: "warning", + role: "agent", + subtype: "strict_review_required", + }, + }, + ]); + return; + } case "item/agentMessage/delta": { await this.#itemEvents.onMessageDelta(context, payload); return; @@ -239,6 +431,7 @@ export class OpenAiAppServerEventBridge { return; } case "item/reasoning/textDelta": { + // Raw reasoning is intentionally private; summary notifications own visible thought output. return; } case "item/completed": { @@ -254,6 +447,14 @@ export class OpenAiAppServerEventBridge { await this.#itemEvents.onFilePatch(context, payload); return; } + case "item/commandExecution/terminalInteraction": { + await this.#onTerminalInteraction(context, payload); + return; + } + case "item/mcpToolCall/progress": { + await this.#onMcpToolProgress(context, payload); + return; + } case "thread/tokenUsage/updated": { await this.#onUsage(context, payload); return; @@ -270,23 +471,141 @@ export class OpenAiAppServerEventBridge { await this.#itemEvents.onTurnPlan(context, payload); return; } + case "mcpServer/startupStatus/updated": { + await this.#push(context, "driver.openai.mcp.server.updated", [ + { + delivery: "best_effort", + kind: "mcp.server.updated", + payload: toBoundedOpenAiTelemetry(payload, [ + "error", + "failureReason", + "name", + "status", + "threadId", + ]), + }, + ]); + return; + } + case "model/rerouted": + case "model/safetyBuffering/updated": { + await this.#push(context, "driver.openai.model.routing_updated", [ + { + delivery: "best_effort", + kind: "model.routing.updated", + payload: toBoundedOpenAiTelemetry(payload, [ + "fasterModel", + "fromModel", + "model", + "reason", + "reasons", + "showBufferingUi", + "threadId", + "toModel", + "turnId", + "useCases", + ]), + }, + ]); + return; + } + case "model/verification": { + await this.#push(context, "driver.openai.model.verification", [ + { + delivery: "best_effort", + kind: "model.verification.updated", + payload: toBoundedOpenAiTelemetry(payload, ["threadId", "turnId", "verifications"]), + }, + ]); + return; + } case "error": { this.#onRuntimeError(context, payload); return; } - default: { + case "deprecationNotice": { + const message = + readString(payload, "summary") ?? + readString(payload, "message") ?? + "OpenAI app-server warning."; + const messageUtf8Bytes = Buffer.byteLength(message, "utf8"); + context.logger.warn("driver.openai.notification.warning", { + method: notificationMethod, + ...(messageUtf8Bytes <= MAX_OPENAI_TELEMETRY_FIELD_BYTES ? { message } : {}), + messageUtf8Bytes, + }); + return; + } + case "windows/worldWritableWarning": { + await this.#onWorldWritableWarning(context, payload); return; } + case "turn/moderationMetadata": + case "skills/changed": + case "thread/name/updated": + case "thread/goal/updated": + case "thread/goal/cleared": + case "thread/reverted": + case "thread/queue/changed": + case "project/changed": + case "thread/project/updated": + case "thread/environment/connected": + case "thread/environment/disconnected": + case "account/updated": + case "account/rateLimits/updated": + case "windowsSandbox/setupCompleted": { + context.logger.debug("driver.openai.notification.handled_without_public_event", { + method: notificationMethod, + }); + return; + } + case "thread/archived": + case "thread/deleted": + case "thread/unarchived": + case "thread/closed": + case "command/exec/outputDelta": + case "process/outputDelta": + case "process/exited": + case "serverRequest/resolved": + case "mcpServer/oauthLogin/completed": + case "mcpServer/event/stream/notification": + case "app/list/updated": + case "externalAgentConfig/import/progress": + case "externalAgentConfig/import/completed": + case "fs/changed": + case "thread/compacted": + case "fuzzyFileSearch/sessionUpdated": + case "fuzzyFileSearch/sessionCompleted": + case "thread/realtime/started": + case "thread/realtime/itemAdded": + case "thread/realtime/item/started": + case "thread/realtime/item/transcript/delta": + case "thread/realtime/item/completed": + case "thread/realtime/transcript/delta": + case "thread/realtime/transcript/done": + case "thread/realtime/outputAudio/delta": + case "thread/realtime/sdp": + case "thread/realtime/error": + case "thread/realtime/closed": + case "account/login/completed": + // Explicitly unsupported request surfaces, deprecated duplicates, or transport bookkeeping. + return; + default: { + const exhaustive: never = notificationMethod; + return exhaustive; + } } } async publishNativeResumeRef(context: AgentDriverContext): Promise { + const threadId = this.#options.requireThreadId(); + const publicThreadId = this.#publicTurnIds.publicId(threadId, "thread"); await this.#push(context, "driver.openai.native_resume_ref.updated", [ { kind: "runtime.resume.updated", payload: { resumePointer: this.#options.requireThreadId(), - threadId: this.#options.requireThreadId(), + threadId: publicThreadId, }, visibility: "owner_debug", }, @@ -297,20 +616,213 @@ export class OpenAiAppServerEventBridge { context: AgentDriverContext, input: { runId?: RunId | undefined; turnId: string }, ): Promise { - if (!this.#turns.markTurnStarted(input.turnId)) { + if (this.#turns.hasTurnStarted(input.turnId)) { + return; + } + + const existing = this.#turnStarts.get(input.turnId); + if (existing !== undefined) { + await existing; return; } - await this.#push(context, "driver.openai.turn.started", [ + const starting = (async () => { + await this.#push(context, "driver.openai.turn.started", [ + { + ...turnEventFields({ + eventName: "turn.started", + publicTurnId: this.publicTurnId(input.turnId), + runId: input.runId, + }), + kind: "run.started", + payload: { + startedAt: new Date().toISOString(), + }, + }, + ]); + this.#turns.markTurnStarted(input.turnId); + })(); + this.#turnStarts.set(input.turnId, starting); + + try { + await starting; + } finally { + if (this.#turnStarts.get(input.turnId) === starting) { + this.#turnStarts.delete(input.turnId); + } + } + } + + async #onAutoApprovalReview( + context: AgentDriverContext, + params: JsonObject, + method: "item/autoApprovalReview/completed" | "item/autoApprovalReview/started", + ): Promise { + const turnId = readNonEmptyString(params, "turnId"); + const runId = turnId === null ? null : this.#turns.activeRunId(turnId); + const action = readRecord(params, "action"); + const review = readRecord(params, "review"); + const targetItemId = readNonEmptyString(params, "targetItemId"); + const actionSummary = + action === null + ? null + : { + ...toBoundedOpenAiTelemetry(action, [ + "approvalId", + "argv", + "command", + "cwd", + "files", + "host", + "permissions", + "port", + "processId", + "program", + "protocol", + "reason", + "server", + "source", + "target", + "toolName", + "toolTitle", + "connectorId", + "connectorName", + "type", + ]), + ...(typeof action["stdin"] === "string" + ? { stdinUtf8Bytes: Buffer.byteLength(action["stdin"], "utf8") } + : {}), + }; + + await this.#push(context, `driver.openai.${method.replaceAll("/", ".")}`, [ { - ...turnEventFields({ - eventName: "turn.started", - runId: input.runId, - turnId: input.turnId, - }), - kind: "run.started", + delivery: "best_effort", + kind: + method === "item/autoApprovalReview/started" + ? "permission.review.started" + : "permission.review.completed", + payload: { + ...toBoundedOpenAiTelemetry(params, [ + "completedAtMs", + "decisionSource", + "reviewId", + "startedAtMs", + "threadId", + "turnId", + ]), + ...(targetItemId === null ? {} : { targetItemId: this.mapToolCallId(targetItemId) }), + ...(actionSummary === null ? {} : { action: actionSummary }), + ...(review === null + ? {} + : { + review: toBoundedOpenAiTelemetry(review, [ + ...(readString(action, "type") === "writeStdin" ? [] : ["rationale"]), + "riskLevel", + "status", + "userAuthorization", + ]), + }), + }, + ...(runId === null ? {} : { runId }), + }, + ]); + } + + async #onHook( + context: AgentDriverContext, + params: JsonObject, + method: "hook/completed" | "hook/started", + ): Promise { + const turnId = readNonEmptyString(params, "turnId"); + const runId = turnId === null ? null : this.#turns.activeRunId(turnId); + const run = readRecord(params, "run"); + + await this.#push(context, `driver.openai.${method.replace("/", ".")}`, [ + { + delivery: "best_effort", + kind: method === "hook/started" ? "hook.started" : "hook.completed", + payload: { + ...toBoundedOpenAiTelemetry(params, ["threadId", "turnId"]), + ...(run === null + ? {} + : { + run: { + ...toBoundedOpenAiTelemetry(run, [ + "completedAt", + "displayOrder", + "durationMs", + "eventName", + "executionMode", + "handlerType", + "id", + "scope", + "source", + "sourcePath", + "startedAt", + "status", + "statusMessage", + ]), + entriesCount: readArray(run, "entries").length, + }, + }), + }, + ...(runId === null ? {} : { runId }), + }, + ]); + } + + async #onMcpToolProgress(context: AgentDriverContext, params: JsonObject): Promise { + const itemId = readNonEmptyString(params, "itemId"); + const message = readString(params, "message"); + + if (itemId === null || message === null) { + return; + } + const publicToolCallId = this.#tools.publicToolCallId(itemId); + + if (publicToolCallId === null) { + return; + } + const parentMessageId = this.#tools.parentMessage(itemId); + await this.#push(context, "driver.openai.mcp_tool.progress", [ + { + delivery: "best_effort", + kind: "tool.call.updated", payload: { - startedAt: new Date().toISOString(), + ...(parentMessageId === null ? {} : { messageId: parentMessageId }), + ...toBoundedOpenAiTelemetry({ rawOutput: message }, ["rawOutput"]), + status: "running", + toolCallId: publicToolCallId, + }, + }, + ]); + } + + async #onTerminalInteraction(context: AgentDriverContext, params: JsonObject): Promise { + const itemId = readNonEmptyString(params, "itemId"); + const processId = readNonEmptyString(params, "processId"); + const threadId = readNonEmptyString(params, "threadId"); + const turnId = readNonEmptyString(params, "turnId"); + + if (itemId === null || processId === null || threadId === null || turnId === null) { + return; + } + const publicItemId = this.#tools.publicToolCallId(itemId); + + if (publicItemId === null) { + return; + } + + await this.#push(context, "driver.openai.terminal.interaction", [ + { + delivery: "best_effort", + kind: "shell.command.updated", + payload: { + ...toBoundedOpenAiTelemetry({ processId }, ["processId"]), + itemId: publicItemId, + status: "running", + threadId: this.#publicTurnIds.publicId(threadId, "thread"), + turnId: this.publicTurnId(turnId), }, }, ]); @@ -368,24 +880,85 @@ export class OpenAiAppServerEventBridge { } } - #onWarning(context: AgentDriverContext, params: JsonObject): void { + async #onWarning( + context: AgentDriverContext, + params: JsonObject, + method: "guardianWarning" | "warning", + ): Promise { + const message = readString(params, "message") ?? "OpenAi app-server warning."; + const messageUtf8Bytes = Buffer.byteLength(message, "utf8"); + context.logger.warn("driver.openai.warning", { - message: readString(params, "message") ?? "OpenAi app-server warning.", + ...(messageUtf8Bytes <= MAX_OPENAI_TELEMETRY_FIELD_BYTES ? { message } : {}), + messageUtf8Bytes, threadIdPresent: readString(params, "threadId") !== null, }); + + await this.#push(context, `driver.openai.${method}`, toOpenAiWarningEvents(method, message)); + } + + async #onWorldWritableWarning(context: AgentDriverContext, params: JsonObject): Promise { + const rawSamplePaths = Array.isArray(params["samplePaths"]) + ? params["samplePaths"].filter((path): path is string => typeof path === "string") + : []; + const samplePaths: string[] = []; + let omittedSampleCount = 0; + + for (const path of rawSamplePaths) { + if ( + samplePaths.length < MAX_WORLD_WRITABLE_SAMPLE_PATHS && + Buffer.byteLength(path, "utf8") <= MAX_OPENAI_TELEMETRY_FIELD_BYTES + ) { + samplePaths.push(path); + } else { + omittedSampleCount += 1; + } + } + const rawExtraCount = params["extraCount"]; + const providerExtraCount = + typeof rawExtraCount === "number" && Number.isSafeInteger(rawExtraCount) && rawExtraCount >= 0 + ? rawExtraCount + : 0; + const extraCount = Math.min(Number.MAX_SAFE_INTEGER, providerExtraCount + omittedSampleCount); + const failedScan = params["failedScan"] === true; + const content = [ + "Windows sandbox protection is incomplete because world-writable directories were found.", + samplePaths.length === 0 ? null : `Affected paths:\n${samplePaths.join("\n")}`, + extraCount === 0 ? null : `${String(extraCount)} additional affected paths were omitted.`, + failedScan ? "The world-writable directory scan did not complete." : null, + ] + .filter((part): part is string => part !== null) + .join("\n\n"); + + context.logger.warn("driver.openai.windows.world_writable", { + extraCount, + failedScan, + samplePaths, + }); + await this.#push(context, "driver.openai.windows.world_writable", [ + { + delivery: "lossless", + kind: "message.added", + payload: { + content, + extraCount, + failedScan, + level: "warning", + role: "agent", + samplePaths, + subtype: "windows_world_writable_warning", + }, + }, + ]); } #onRuntimeError(context: AgentDriverContext, params: JsonObject): void { const error = readRecord(params, "error"); - const message = - readString(error, "message") ?? readString(params, "message") ?? "OpenAi app-server error."; - const additionalDetails = readString(error, "additionalDetails"); const turnId = readString(params, "turnId"); const willRetry = params["willRetry"] === true; context.logger.warn("driver.openai.error.awaiting_turn_completion", { - additionalDetails, - message, + ...toBoundedOpenAiTelemetry(error ?? params, ["additionalDetails", "message"]), threadIdPresent: readString(params, "threadId") !== null, turnIdPresent: turnId !== null, willRetry, @@ -418,22 +991,29 @@ export class OpenAiAppServerEventBridge { if (!this.#turns.beginSettlement(turnId)) { return; } + const publicTurnId = this.publicTurnId(turnId); - const error = turn ? readRecord(turn, "error") : null; try { if (status === "interrupted") { await this.#options.beforeInterruptedTurn?.(context, turnId); - await this.#push(context, "driver.openai.turn.interrupted", [ - ...this.#itemEvents.finishOpen(), + await this.#options.pushTerminal( + context, + "driver.openai.turn.interrupted", + turnClosureEvents({ + eventName: "turn.interrupted", + events: this.#itemEvents.terminalEvents({ kind: "cancelled" }), + publicTurnId, + runId, + }), { - ...turnEventFields({ eventName: "turn.interrupted", runId, turnId }), + ...turnEventFields({ eventName: "turn.interrupted", publicTurnId, runId }), kind: "run.cancelled", payload: { requestedBy: "provider", stopReason: "cancelled", }, }, - ]); + ); this.#finishSettlement(turnId, { error: new DriverTurnCancelledError("OpenAI turn was interrupted."), kind: "failed", @@ -442,30 +1022,31 @@ export class OpenAiAppServerEventBridge { } if (status === "failed") { - const message = toOpenAiErrorMessage( - readString(error, "message") ?? "OpenAi turn failed.", - readString(error, "additionalDetails"), - ); - await this.#push(context, "driver.openai.turn.failed", [ - ...this.#itemEvents.finishOpen(), + const error = toOpenAiProtocolError(readRecord(turn, "error")); + await this.#options.pushTerminal( + context, + "driver.openai.turn.failed", + turnClosureEvents({ + eventName: "turn.failed", + events: this.#itemEvents.terminalEvents({ error, kind: "failed" }), + publicTurnId, + runId, + }), { ...turnEventFields({ eventName: "turn.failed", + publicTurnId, runId, - turnId, }), kind: "run.failed", payload: { - error: { - code: "openai.turn_failed", - message, - }, - recoverable: false, + error, + recoverable: error.retryable, }, }, - ]); + ); this.#finishSettlement(turnId, { - error: new Error(message), + error: new Error(error.message), kind: "failed", }); return; @@ -485,13 +1066,20 @@ export class OpenAiAppServerEventBridge { }); } - await this.#push(context, "driver.openai.turn.completed", [ - ...this.#itemEvents.finishOpen(), + await this.#options.pushTerminal( + context, + "driver.openai.turn.completed", + turnClosureEvents({ + eventName: "turn.completed", + events: this.#itemEvents.terminalEvents({ kind: "completed" }), + publicTurnId, + runId, + }), { ...turnEventFields({ eventName: "turn.completed", + publicTurnId, runId, - turnId, }), kind: "run.completed", payload: { @@ -499,12 +1087,11 @@ export class OpenAiAppServerEventBridge { ? {} : { finalMessageId: authoritativeFinalMessage.id, - finalMessageText: authoritativeFinalMessage.text, }), stopReason: "end_turn", }, }, - ]); + ); this.#finishSettlement(turnId, { kind: "completed" }); } catch (pushError) { this.#turns.cancelSettlement(turnId); @@ -527,12 +1114,15 @@ export class OpenAiAppServerEventBridge { await this.#push(context, "driver.openai.turn.diff.updated", [ { + delivery: "best_effort", kind: "diagnostic.reported", payload: { - diff, + details: { + utf8Bytes: Buffer.byteLength(diff, "utf8"), + }, message: "OpenAI turn diff updated.", severity: "info", - turnId, + turnId: this.publicTurnId(turnId), }, runId, visibility: "owner_debug", @@ -555,19 +1145,26 @@ export class OpenAiAppServerEventBridge { async #onUsage(context: AgentDriverContext, params: JsonObject): Promise { const turnId = readNonEmptyString(params, "turnId"); - const runId = turnId === null ? null : this.#turns.activeRunId(turnId); - if (runId === null) { + if (turnId === null) { + return; + } + const runId = this.#turns.activeRunId(turnId); + const update = this.#usage.prepareUpdate(runId === null ? null : turnId, params); + + if (runId === null || update.usage === null) { + update.commit(); return; } await this.#push(context, "driver.openai.usage.updated", [ { kind: "usage.updated", - payload: toOpenAiSessionUsageSummary(params), + payload: update.usage, runId, }, ]); + update.commit(); } async #push( @@ -588,5 +1185,6 @@ export class OpenAiAppServerEventBridge { ): void { this.#turns.finishSettlement(turnId, terminalTurn); this.#itemEvents.reset(); + this.#usage.release(turnId); } } diff --git a/src/runtimes/openai/app-server-event-mapping.ts b/src/runtimes/openai/app-server-event-mapping.ts index c011b90..992f7aa 100644 --- a/src/runtimes/openai/app-server-event-mapping.ts +++ b/src/runtimes/openai/app-server-event-mapping.ts @@ -1,13 +1,42 @@ -import { readRecord } from "./app-server-json"; +import type { ProtocolError } from "../../contract"; +import { isRecord, readString } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; +const retryableCodexErrors = new Set([ + "httpConnectionFailed", + "responseStreamConnectionFailed", + "internalServerError", + "responseStreamDisconnected", +]); +const MAX_OPENAI_ERROR_TEXT_BYTES = 16 * 1_024; + +function boundOpenAiErrorText( + text: string, + label: string, +): { + readonly text: string; + readonly utf8Bytes: number | null; +} { + const utf8Bytes = Buffer.byteLength(text, "utf8"); + + return utf8Bytes <= MAX_OPENAI_ERROR_TEXT_BYTES + ? { text, utf8Bytes: null } + : { + text: `${label} was omitted because it contained ${String(utf8Bytes)} UTF-8 bytes.`, + utf8Bytes, + }; +} + function readNonNegativeNumber(value: JsonObject | null, key: string): number | null { const entry = value?.[key]; - return typeof entry === "number" && Number.isFinite(entry) && entry >= 0 ? entry : null; + return typeof entry === "number" && Number.isSafeInteger(entry) && entry >= 0 ? entry : null; } -export function toOpenAiErrorMessage(message: string, additionalDetails: string | null): string { +export function toOpenAiErrorMessage( + message: string, + additionalDetails: string | null | undefined, +): string { const details = additionalDetails?.trim(); if (!details || details === message) { @@ -17,29 +46,86 @@ export function toOpenAiErrorMessage(message: string, additionalDetails: string return `${message}\n${details}`; } -export function toOpenAiSessionUsageSummary(params: JsonObject) { - const tokenUsage = readRecord(params, "tokenUsage"); - const usage = - readRecord(tokenUsage, "last") ?? - readRecord(tokenUsage, "total") ?? - readRecord(params, "usage") ?? - params; +function classifyOpenAiError(info: unknown): { + codexErrorInfo?: string; + httpStatusCode?: number; + retryable: boolean; + turnKind?: string; +} { + if (info == null) { + return { retryable: false }; + } + + const [codexErrorInfo, metadata] = + typeof info === "string" + ? [info, null] + : (Object.entries(isRecord(info) ? info : {})[0] ?? ["unknown", null]); + const metadataRecord = isRecord(metadata) ? metadata : null; + const httpStatusCode = readNonNegativeNumber(metadataRecord, "httpStatusCode"); + const turnKind = metadataRecord?.["turnKind"]; + + return { + codexErrorInfo, + ...(httpStatusCode === null ? {} : { httpStatusCode }), + retryable: retryableCodexErrors.has(codexErrorInfo), + ...(typeof turnKind === "string" ? { turnKind } : {}), + }; +} + +export function toOpenAiProtocolError(error: JsonObject | null): ProtocolError { + if (error === null) { + return { + code: "openai.turn_failed", + message: "OpenAI turn failed.", + retryable: false, + }; + } + + const rawMessage = readString(error, "message") ?? "OpenAI turn failed."; + const rawAdditionalDetails = readString(error, "additionalDetails"); + const message = boundOpenAiErrorText(rawMessage, "OpenAI provider error message"); + const additionalDetails = + rawAdditionalDetails === null + ? null + : boundOpenAiErrorText(rawAdditionalDetails, "OpenAI provider error details"); + const { retryable, ...classification } = classifyOpenAiError(error["codexErrorInfo"]); + const details = { + ...(additionalDetails === null ? {} : { additionalDetails: additionalDetails.text }), + ...(additionalDetails?.utf8Bytes === null || additionalDetails === null + ? {} + : { additionalDetailsUtf8Bytes: additionalDetails.utf8Bytes }), + ...classification, + ...(message.utf8Bytes === null ? {} : { messageUtf8Bytes: message.utf8Bytes }), + }; + + return { + code: "openai.turn_failed", + ...(Object.keys(details).length === 0 ? {} : { details }), + message: toOpenAiErrorMessage(message.text, additionalDetails?.text), + retryable, + }; +} + +export function toOpenAiSessionUsageSummary(input: { + contextWindow: number | null; + usage: JsonObject | null; + used: number | null; +}) { + const { usage } = input; return { cachedReadTokens: readNonNegativeNumber(usage, "cachedInputTokens"), - cachedWriteTokens: null, + cachedWriteTokens: readNonNegativeNumber(usage, "cacheWriteInputTokens"), costAmount: null, costCurrency: null, inputTokens: readNonNegativeNumber(usage, "inputTokens"), outputTokens: readNonNegativeNumber(usage, "outputTokens"), - size: null, + size: input.contextWindow, source: "session_update" as const, - thoughtTokens: - readNonNegativeNumber(usage, "reasoningOutputTokens") ?? - readNonNegativeNumber(usage, "reasoningTokens"), + thoughtTokens: readNonNegativeNumber(usage, "reasoningOutputTokens"), totalTokens: readNonNegativeNumber(usage, "totalTokens"), usageContract: "openai_runtime_total_with_cached_breakdown" as const, - used: null, + used: input.used, }; } diff --git a/src/runtimes/openai/app-server-event-state.ts b/src/runtimes/openai/app-server-event-state.ts index b7cac9a..7ede657 100644 --- a/src/runtimes/openai/app-server-event-state.ts +++ b/src/runtimes/openai/app-server-event-state.ts @@ -1,7 +1,13 @@ +import { createHash } from "node:crypto"; + import type { DriverEventInput } from "../../protocol/events"; -import type { MessageId } from "../../protocol/id"; +import { createDriverId, type MessageId } from "../../protocol/id"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; +import type { ProtocolError } from "../../contract"; import { RuntimeAssistantMessageIdIndex } from "../runtime-turn-transcript"; +import { toOpenAiSessionUsageSummary } from "./app-server-event-mapping"; +import { readRecord } from "./app-server-json"; +import type { JsonObject } from "./app-server-json"; export type OpenAiEventPush = ( context: AgentDriverContext, @@ -9,12 +15,154 @@ export type OpenAiEventPush = ( events: DriverEventInput[], ) => Promise; +export type OpenAiTerminalOutcome = + | { kind: "cancelled" } + | { kind: "completed" } + | { error: ProtocolError; kind: "failed" }; + +export type OpenAiMessagePhase = "commentary" | "final" | null; + +const usageKeys = [ + "cacheWriteInputTokens", + "cachedInputTokens", + "inputTokens", + "outputTokens", + "reasoningOutputTokens", + "totalTokens", +] as const; +const MAX_OPENAI_PUBLIC_NATIVE_ID_BYTES = 256; +export const MAX_OPENAI_DURABLE_EVENT_BYTES = 1_020 * 1_024; + +export function assertOpenAiDurableEventFits(event: DriverEventInput, subject: string): void { + const bytes = Buffer.byteLength(JSON.stringify(event), "utf8"); + + if (bytes > MAX_OPENAI_DURABLE_EVENT_BYTES) { + throw new RangeError( + `OpenAI ${subject} exceeds durable event capacity (${String(bytes)} UTF-8 bytes).`, + ); + } +} + +function openAiNativeIdKey(nativeId: string): string { + return Buffer.byteLength(nativeId, "utf8") <= MAX_OPENAI_PUBLIC_NATIVE_ID_BYTES + ? nativeId + : createHash("sha256").update(nativeId).digest("hex"); +} + +export class OpenAiPublicIdState { + readonly #publicIds = new Map(); + + publicId(nativeId: string, namespace = "id"): string { + if (Buffer.byteLength(nativeId, "utf8") <= MAX_OPENAI_PUBLIC_NATIVE_ID_BYTES) { + return nativeId; + } + + const key = `${namespace}:${openAiNativeIdKey(nativeId)}`; + const existing = this.#publicIds.get(key); + if (existing !== undefined) { + return existing; + } + + const publicId = createDriverId(); + this.#publicIds.set(key, publicId); + return publicId; + } + + reset(): void { + this.#publicIds.clear(); + } +} + +type OpenAiTokenUsage = Partial>; + +function readUsage(value: JsonObject | null): OpenAiTokenUsage { + return Object.fromEntries( + usageKeys.flatMap((key) => { + const entry = value?.[key]; + return typeof entry === "number" && Number.isSafeInteger(entry) && entry >= 0 + ? [[key, entry]] + : []; + }), + ); +} + +function subtractUsage(total: OpenAiTokenUsage, baseline: OpenAiTokenUsage): OpenAiTokenUsage { + return Object.fromEntries( + usageKeys.flatMap((key) => { + const value = total[key]; + return value === undefined ? [] : [[key, Math.max(0, value - (baseline[key] ?? 0))]]; + }), + ); +} + +function monotonicUsage(previous: OpenAiTokenUsage, next: OpenAiTokenUsage): OpenAiTokenUsage { + return Object.fromEntries( + usageKeys.flatMap((key) => { + const value = next[key] ?? previous[key]; + return value === undefined ? [] : [[key, Math.max(value, previous[key] ?? 0)]]; + }), + ); +} + +export class OpenAiSessionUsageState { + #lastTotal: OpenAiTokenUsage | null = null; + readonly #turns = new Map(); + + prepareUpdate(turnId: string | null, params: JsonObject) { + const tokenUsage = readRecord(params, "tokenUsage"); + const total = readUsage(readRecord(tokenUsage, "total")); + const last = readUsage(readRecord(tokenUsage, "last")); + const previous = turnId === null ? undefined : this.#turns.get(turnId); + const baseline = previous?.baseline ?? this.#lastTotal ?? subtractUsage(total, last); + const current = monotonicUsage(previous?.current ?? {}, subtractUsage(total, baseline)); + + if (turnId === null) { + return { + commit: () => { + this.#lastTotal = monotonicUsage(this.#lastTotal ?? {}, total); + }, + usage: null, + }; + } + const changed = usageKeys.some((key) => (current[key] ?? 0) > (previous?.current[key] ?? 0)); + + return { + commit: () => { + this.#lastTotal = monotonicUsage(this.#lastTotal ?? {}, total); + this.#turns.set(turnId, { baseline, current }); + }, + usage: changed + ? toOpenAiSessionUsageSummary({ + contextWindow: + typeof tokenUsage?.["modelContextWindow"] === "number" && + Number.isSafeInteger(tokenUsage["modelContextWindow"]) && + tokenUsage["modelContextWindow"] >= 0 + ? tokenUsage["modelContextWindow"] + : null, + usage: current, + used: last["totalTokens"] ?? null, + }) + : null, + }; + } + + release(turnId: string): void { + this.#turns.delete(turnId); + } + + reset(): void { + this.#turns.clear(); + this.#lastTotal = null; + } +} + export class OpenAiMessageState { readonly #completedSnapshots = new Map< string, { itemId: string; messageId: MessageId; + phase: OpenAiMessagePhase; sequence: number; text: string; turnId: string; @@ -25,7 +173,11 @@ export class OpenAiMessageState { readonly #itemByMessageId = new Map(); readonly #itemMessageIds = new Map(); readonly #reasoningEnded = new Set(); + readonly #reasoningIds = new RuntimeAssistantMessageIdIndex(); + readonly #reasoningParts = new Map>(); readonly #reasoningStarted = new Set(); + readonly #reasoningTextById = new Map(); + readonly #starting = new Map>(); readonly #started = new Set(); readonly #textById = new Map(); readonly #turnByMessageId = new Map(); @@ -50,47 +202,89 @@ export class OpenAiMessageState { this.#textById.set(messageId, text); } - ensureReasoning(messageId: string, events: DriverEventInput[]): void { + ensureReasoning(messageId: string, events: DriverEventInput[], commit = true): void { if (this.#reasoningEnded.has(messageId) || this.#reasoningStarted.has(messageId)) { return; } - this.#reasoningStarted.add(messageId); + if (commit) { + this.#reasoningStarted.add(messageId); + } events.push({ kind: "thought.started", payload: { channel: "summary", thoughtId: messageId, }, + sourceEventId: `openai.thought.started:${messageId}`, }); } - finishOpen(): DriverEventInput[] { + appendReasoningText(messageId: string, delta: string): void { + if (delta.length === 0 || this.#reasoningEnded.has(messageId)) { + return; + } + + this.#reasoningTextById.set( + messageId, + `${this.#reasoningTextById.get(messageId) ?? ""}${delta}`, + ); + } + + beginReasoningPart(messageId: string, summaryIndex: number, commit = true): boolean { + if (this.#reasoningEnded.has(messageId)) { + return false; + } + + const parts = this.#reasoningParts.get(messageId) ?? new Set(); + + if (parts.has(summaryIndex)) { + return false; + } + + if (commit) { + this.#reasoningParts.set(messageId, parts); + parts.add(summaryIndex); + } + return true; + } + + currentReasoningText(messageId: string): string { + return this.#reasoningTextById.get(messageId) ?? ""; + } + + terminalEvents(outcome: OpenAiTerminalOutcome): DriverEventInput[] { const events: DriverEventInput[] = []; for (const messageId of this.#started) { - this.markEnded(messageId); + events.push( + outcome.kind === "completed" + ? { + kind: "message.completed", + payload: { messageId, role: "agent" }, + } + : outcome.kind === "cancelled" + ? { + kind: "message.cancelled", + payload: { messageId, role: "agent" }, + } + : { + kind: "message.failed", + payload: { error: outcome.error, messageId, role: "agent" }, + }, + ); + } + + for (const thoughtId of this.#reasoningStarted) { events.push({ - kind: "message.completed", + kind: outcome.kind === "completed" ? "thought.completed" : "thought.cancelled", payload: { - messageId, - role: "agent", + channel: "summary", + thoughtId, }, }); } - for (const thoughtId of this.#reasoningStarted) { - if (this.markReasoningEnded(thoughtId)) { - events.push({ - kind: "thought.completed", - payload: { - channel: "summary", - thoughtId, - }, - }); - } - } - return events; } @@ -107,11 +301,11 @@ export class OpenAiMessageState { } const nextSequence = (this.#turnMessageSequences.get(turnId) ?? 0) + 1; - this.#turnMessageSequences.set(turnId, nextSequence); const generated = this.#turnMessages.getOrCreate(`${turnId}:${nextSequence}`); + await this.ensureStarted(context, generated, push); + this.#turnMessageSequences.set(turnId, nextSequence); this.#turnMessageIds.set(turnId, generated); this.#turnByMessageId.set(generated, turnId); - await this.ensureStarted(context, generated, push); return generated; } @@ -121,7 +315,6 @@ export class OpenAiMessageState { push: OpenAiEventPush, ): Promise { const itemKey = `${input.turnId}:${input.itemId}`; - this.#observeItem(itemKey, input.turnId); const existing = this.#itemMessageIds.get(itemKey); if (existing !== undefined) { @@ -140,42 +333,72 @@ export class OpenAiMessageState { ? activeMessageId : this.#turnMessages.getOrCreate(`item:${itemKey}`); + await this.ensureStarted(context, messageId, push); + this.#observeItem(itemKey, input.turnId); this.#itemMessageIds.set(itemKey, messageId); this.#itemByMessageId.set(messageId, itemKey); this.#turnMessageIds.set(input.turnId, messageId); this.#turnByMessageId.set(messageId, input.turnId); - await this.ensureStarted(context, messageId, push); return messageId; } recordSnapshot(input: { itemId: string; messageId: MessageId; + phase: OpenAiMessagePhase; text: string; turnId: string; - }): void { + }): boolean { const itemKey = `${input.turnId}:${input.itemId}`; const sequence = this.#observeItem(itemKey, input.turnId); + if (!this.needsSnapshot(input)) { + return false; + } this.#completedSnapshots.set(itemKey, { ...input, sequence }); + return true; + } + + needsSnapshot(input: { + itemId: string; + messageId: MessageId; + phase: OpenAiMessagePhase; + text: string; + turnId: string; + }): boolean { + const previous = this.#completedSnapshots.get(`${input.turnId}:${input.itemId}`); + return ( + previous?.messageId !== input.messageId || + previous.phase !== input.phase || + previous.text !== input.text + ); } finalSnapshot(turnId: string): { id: MessageId; text: string } | null { + const turnSnapshots = [...this.#completedSnapshots.values()].filter( + (snapshot) => snapshot.turnId === turnId, + ); + const hasExplicitPhase = turnSnapshots.some((snapshot) => snapshot.phase !== null); let finalSnapshot: | { itemId: string; messageId: MessageId; + phase: OpenAiMessagePhase; sequence: number; text: string; turnId: string; } | undefined; - for (const snapshot of this.#completedSnapshots.values()) { + for (const snapshot of turnSnapshots) { if ( - snapshot.turnId === turnId && - (finalSnapshot === undefined || snapshot.sequence > finalSnapshot.sequence) + (hasExplicitPhase && snapshot.phase !== "final") || + (!hasExplicitPhase && snapshot.phase !== null) ) { + continue; + } + + if (finalSnapshot === undefined || snapshot.sequence > finalSnapshot.sequence) { finalSnapshot = snapshot; } } @@ -194,16 +417,35 @@ export class OpenAiMessageState { return; } - this.#started.add(messageId); - await push(context, "driver.openai.message.started", [ - { - kind: "message.started", - payload: { - messageId, - role: "agent", + const existing = this.#starting.get(messageId); + + if (existing !== undefined) { + await existing; + return; + } + + const starting = (async () => { + await push(context, "driver.openai.message.started", [ + { + kind: "message.started", + payload: { + messageId, + role: "agent", + }, + sourceEventId: `openai.message.started:${messageId}`, }, - }, - ]); + ]); + this.#started.add(messageId); + })(); + this.#starting.set(messageId, starting); + + try { + await starting; + } finally { + if (this.#starting.get(messageId) === starting) { + this.#starting.delete(messageId); + } + } } markEnded(messageId: MessageId): boolean { @@ -234,6 +476,14 @@ export class OpenAiMessageState { return this.#reasoningEnded.has(messageId); } + reasoningId(itemId: string): string { + return this.#reasoningIds.getOrCreate(itemId); + } + + isReasoningStarted(messageId: string): boolean { + return this.#reasoningStarted.has(messageId); + } + markReasoningEnded(messageId: string): boolean { if (this.#reasoningEnded.has(messageId) || !this.#reasoningStarted.has(messageId)) { return false; @@ -255,7 +505,11 @@ export class OpenAiMessageState { this.#itemByMessageId.clear(); this.#itemMessageIds.clear(); this.#reasoningEnded.clear(); + this.#reasoningIds.reset(); + this.#reasoningParts.clear(); this.#reasoningStarted.clear(); + this.#reasoningTextById.clear(); + this.#starting.clear(); this.#started.clear(); this.#textById.clear(); this.#turnByMessageId.clear(); @@ -281,6 +535,11 @@ export class OpenAiMessageState { export class OpenAiItemState { readonly #completed = new Set(); + readonly #ids = new OpenAiPublicIdState(); + + isCompleted(itemId: string): boolean { + return this.#completed.has(itemId); + } markCompleted(itemId: string): boolean { if (this.#completed.has(itemId)) { @@ -291,16 +550,29 @@ export class OpenAiItemState { return true; } + publicId(nativeId: string, namespace: "item" | "turn" = "item"): string { + return this.#ids.publicId(nativeId, namespace); + } + reset(): void { this.#completed.clear(); + this.#ids.reset(); } } export class OpenAiToolState { - readonly #parentMessages = new Map(); + readonly #parentMessages = new Map< + string, + { parentMessageId: MessageId; publicToolCallId: string } + >(); + readonly #starting = new Map>(); parentMessage(toolCallId: string): MessageId | null { - return this.#parentMessages.get(toolCallId) ?? null; + return this.#parentMessages.get(toolCallId)?.parentMessageId ?? null; + } + + publicToolCallId(toolCallId: string): string | null { + return this.#parentMessages.get(toolCallId)?.publicToolCallId ?? null; } async ensureStarted( @@ -308,60 +580,94 @@ export class OpenAiToolState { push: OpenAiEventPush, input: { parentMessageId: MessageId; + publicToolCallId: string; reason: string; + sourceScope: string; toolCallId: string; toolCallName: string; }, ): Promise { - const started = this.#parentMessages.has(input.toolCallId); - this.#parentMessages.set(input.toolCallId, input.parentMessageId); + if (this.#parentMessages.has(input.toolCallId)) { + return; + } - if (started) { + const existing = this.#starting.get(input.toolCallId); + + if (existing !== undefined) { + await existing; return; } - await push(context, input.reason, [ - { - kind: "item.started", - payload: { - itemId: input.toolCallId, - itemType: "tool_call", - parentMessageId: input.parentMessageId, - title: input.toolCallName, + const starting = (async () => { + const events: DriverEventInput[] = [ + { + kind: "item.started", + payload: { + itemId: input.publicToolCallId, + itemType: "tool_call", + parentMessageId: input.parentMessageId, + title: input.toolCallName, + }, + sourceEventId: `openai.item.started:${input.sourceScope}:${input.publicToolCallId}`, }, - }, - { - kind: "tool.call.updated", - payload: { - kind: "tool", - parentMessageId: input.parentMessageId, - status: "running", - title: input.toolCallName, - toolCallId: input.toolCallId, + { + kind: "tool.call.updated", + payload: { + kind: "tool", + parentMessageId: input.parentMessageId, + status: "running", + title: input.toolCallName, + toolCallId: input.publicToolCallId, + }, + sourceEventId: `openai.tool.started:${input.sourceScope}:${input.publicToolCallId}`, }, - }, - ]); + ]; + for (const event of events) { + assertOpenAiDurableEventFits(event, "tool start"); + } + await push(context, input.reason, events); + this.#parentMessages.set(input.toolCallId, { + parentMessageId: input.parentMessageId, + publicToolCallId: input.publicToolCallId, + }); + })(); + this.#starting.set(input.toolCallId, starting); + + try { + await starting; + } finally { + if (this.#starting.get(input.toolCallId) === starting) { + this.#starting.delete(input.toolCallId); + } + } } - failOpen(): DriverEventInput[] { - const events = [...this.#parentMessages.keys()].flatMap((toolCallId) => [ - { - kind: "tool.call.updated", - payload: { - status: "failed", - toolCallId, + terminalEvents(outcome: OpenAiTerminalOutcome): DriverEventInput[] { + const status = + outcome.kind === "completed" + ? "completed" + : outcome.kind === "cancelled" + ? "cancelled" + : "failed"; + const events = [...this.#parentMessages.values()].flatMap( + ({ publicToolCallId }) => [ + { + kind: "tool.call.updated", + payload: { + status, + toolCallId: publicToolCallId, + }, }, - }, - { - kind: "item.completed", - payload: { - itemId: toolCallId, - itemType: "tool_call", - status: "failed", + { + kind: "item.completed", + payload: { + itemId: publicToolCallId, + itemType: "tool_call", + status, + }, }, - }, - ]); - this.#parentMessages.clear(); + ], + ); return events; } @@ -371,11 +677,17 @@ export class OpenAiToolState { reset(): void { this.#parentMessages.clear(); + this.#starting.clear(); } } export class OpenAiPlanState { readonly #plans = new Map(); + readonly #turnVersions = new Map(); + + currentLength(itemId: string): number { + return this.#plans.get(itemId)?.content.length ?? 0; + } appendDelta(itemId: string, delta: string): void { const current = this.#plans.get(itemId); @@ -385,11 +697,29 @@ export class OpenAiPlanState { }); } - createUpdatedEvent(): DriverEventInput { + createDeltaEvent(itemId: string, delta: string): DriverEventInput { + const plans = new Map(this.#plans); + const current = plans.get(itemId); + plans.set(itemId, { + content: `${current?.content ?? ""}${delta}`, + status: "in_progress", + }); + return this.#createEvent(plans); + } + + createCompletedEvent(itemId: string, content: string): DriverEventInput { + const plans = new Map(this.#plans); + plans.set(itemId, { content, status: "completed" }); + return this.#createEvent(plans); + } + + #createEvent( + plans: ReadonlyMap, + ): DriverEventInput { return { kind: "plan.updated", payload: { - entries: [...this.#plans.values()] + entries: [...plans.values()] .filter((entry) => entry.content.trim().length > 0) .map((entry) => ({ content: entry.content.trim(), @@ -410,5 +740,14 @@ export class OpenAiPlanState { reset(): void { this.#plans.clear(); + this.#turnVersions.clear(); + } + + turnVersion(turnId: string): number { + return this.#turnVersions.get(turnId) ?? 0; + } + + advanceTurnVersion(turnId: string): void { + this.#turnVersions.set(turnId, this.turnVersion(turnId) + 1); } } diff --git a/src/runtimes/openai/app-server-item-events.ts b/src/runtimes/openai/app-server-item-events.ts index b4bf96a..b0a4163 100644 --- a/src/runtimes/openai/app-server-item-events.ts +++ b/src/runtimes/openai/app-server-item-events.ts @@ -1,25 +1,327 @@ import type { AgentDriverContext } from "../../core/agent-driver-backend"; import type { DriverEventInput } from "../../protocol/events"; +import { chunkJsonText } from "../provider-json"; import { toOpenAiPlanStatus } from "./app-server-event-mapping"; +import { + assertOpenAiDurableEventFits, + MAX_OPENAI_DURABLE_EVENT_BYTES, +} from "./app-server-event-state"; import type { OpenAiEventPush, OpenAiItemState, OpenAiMessageState, OpenAiPlanState, + OpenAiTerminalOutcome, OpenAiToolState, } from "./app-server-event-state"; import { isRecord, readArray, readNonEmptyString, readRecord, readString } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; import { + toOpenAiCollaborationOutput, toOpenAiFileChangeEvents, + toOpenAiMessagePhase, toOpenAiToolName, + toOpenAiToolRawInput, toOpenAiToolResultText, + toOpenAiToolStructuredOutput, } from "./event-translator"; import { filterOpenAiPrivateCitations, OpenAiPrivateCitationStreamFilter, } from "./private-citation-filter"; +const MAX_OPENAI_MESSAGE_EVENT_TEXT_BYTES = 512 * 1_024; + +export function chunkOpenAiText( + text: string, + firstChunkBytes = MAX_OPENAI_MESSAGE_EVENT_TEXT_BYTES, + remainingChunkBytes = MAX_OPENAI_MESSAGE_EVENT_TEXT_BYTES, +): string[] { + return chunkJsonText(text, firstChunkBytes, remainingChunkBytes); +} + +function toOpenAiMessageSnapshotEvents(input: { + readonly item: JsonObject; + readonly itemId: string; + readonly messageId: string; + readonly phase: ReturnType; + readonly sourcePrefix: string; + readonly text: string; +}): DriverEventInput[] { + const createAddedEvent = (content: string): DriverEventInput => ({ + delivery: "lossless", + kind: "message.added", + payload: { + content, + ...(input.item["memoryCitation"] === null || input.item["memoryCitation"] === undefined + ? {} + : { memoryCitation: input.item["memoryCitation"] }), + messageId: input.messageId, + ...(input.phase === null ? {} : { phase: input.phase }), + role: "agent", + }, + sourceEventId: `${input.sourcePrefix}:0`, + }); + const createDeltaEvent = (contentDelta: string, index: number): DriverEventInput => ({ + delivery: "lossless", + kind: "message.delta", + payload: { + contentDelta, + messageId: input.messageId, + role: "agent", + }, + sourceEventId: `${input.sourcePrefix}:${String(index)}`, + }); + const emptyAddedEvent = createAddedEvent(""); + const metadataBytes = Buffer.byteLength(JSON.stringify(emptyAddedEvent), "utf8"); + assertOpenAiDurableEventFits(emptyAddedEvent, `message snapshot ${input.itemId}`); + const emptyDeltaEvent = createDeltaEvent("", input.text.length); + const deltaMetadataBytes = Buffer.byteLength(JSON.stringify(emptyDeltaEvent), "utf8"); + assertOpenAiDurableEventFits(emptyDeltaEvent, `message snapshot ${input.itemId}`); + const chunks = chunkOpenAiText( + input.text, + Math.min(MAX_OPENAI_MESSAGE_EVENT_TEXT_BYTES, MAX_OPENAI_DURABLE_EVENT_BYTES - metadataBytes), + Math.min( + MAX_OPENAI_MESSAGE_EVENT_TEXT_BYTES, + MAX_OPENAI_DURABLE_EVENT_BYTES - deltaMetadataBytes, + ), + ); + + const events: DriverEventInput[] = [ + createAddedEvent(chunks[0]!), + ...chunks.slice(1).map((contentDelta, index) => createDeltaEvent(contentDelta, index + 1)), + ]; + assertOpenAiDurableEventFits(events[0]!, `message snapshot ${input.itemId}`); + return events; +} + +interface OpenAiItemCompletionCommit { + readonly commit: () => void; + readonly events: DriverEventInput[]; +} + +function withOpenAiEventIds( + events: readonly DriverEventInput[], + sourcePrefix: string, +): DriverEventInput[] { + return events.map((event, index) => ({ + ...event, + sourceEventId: event.sourceEventId ?? `${sourcePrefix}:${String(index)}`, + })); +} + +function toOpenAiItemLifecycleEvents( + item: JsonObject, + itemId: string, + phase: "completed" | "started", +): DriverEventInput[] { + const itemType = readString(item, "type"); + + switch (itemType) { + // These are translated by their dedicated message, plan, reasoning, or tool paths. + case "agentMessage": + case "plan": + case "reasoning": + case "commandExecution": + case "fileChange": + case "mcpToolCall": + case "dynamicToolCall": + case "collabAgentToolCall": + case "webSearch": + case "imageView": + case "sleep": + return []; + // The input path owns user messages, and hook/* notifications own hook lifecycle. + // Their ThreadItem copies are provider echoes and must not publish duplicates. + case "userMessage": + case "hookPrompt": + return []; + case "subAgentActivity": { + // App-server emits a started/completed pair for one display activity. Publish it once. + // Completion is authoritative and is also the only phase present in turn snapshots. + if (phase === "started") { + return []; + } + + const activityKind = readString(item, "kind"); + const agentId = readNonEmptyString(item, "agentThreadId"); + const agentPath = readNonEmptyString(item, "agentPath"); + + if ( + agentId === null || + agentPath === null || + (activityKind !== "started" && + activityKind !== "interacted" && + activityKind !== "interrupted" && + activityKind !== "completed") + ) { + throw new Error("OpenAI sub-agent activity is malformed."); + } + + return [ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { + ...(activityKind === "started" + ? { active: true, status: "running" } + : activityKind === "interacted" + ? {} + : { + active: false, + status: activityKind === "interrupted" ? "cancelled" : "completed", + }), + activityKind, + agentId, + agentPath, + taskId: agentId, + title: `Sub-agent ${activityKind}`, + }, + }, + ]; + } + case "imageGeneration": { + if (phase === "started") { + return []; + } + + const status = readString(item, "status"); + const revisedPrompt = readString(item, "revisedPrompt"); + const transparentBackground = item["transparentBackground"]; + const imageMetadata = { + imageId: itemId, + ...(revisedPrompt === null ? {} : { revisedPrompt }), + ...(typeof transparentBackground === "boolean" ? { transparentBackground } : {}), + }; + + if (status === "failed") { + return [ + { + kind: "diagnostic.reported", + payload: { + code: "openai.image_generation.failed", + details: { + ...imageMetadata, + }, + message: "OpenAI image generation failed.", + severity: "error", + source: "openai", + }, + visibility: "owner_debug", + }, + ]; + } + + if (status !== "completed" || readNonEmptyString(item, "result") === null) { + throw new Error("OpenAI completed image generation did not contain a PNG result."); + } + + throw new Error( + "OpenAI image generation completed without a supported durable image transport.", + ); + } + case "enteredReviewMode": + case "exitedReviewMode": + return phase === "completed" + ? [ + { + kind: "review.updated", + payload: { + mode: itemType === "enteredReviewMode" ? "entered" : "exited", + review: readString(item, "review") ?? "", + reviewId: itemId, + status: "completed", + }, + }, + ] + : []; + case "contextCompaction": + return phase === "completed" + ? [ + { + kind: "context.compacted", + payload: { itemId, status: "completed" }, + }, + ] + : []; + default: + return [ + { + kind: "diagnostic.reported", + payload: { + code: "openai.item.unknown", + details: { itemId, itemType, phase }, + message: `OpenAI ${String(itemType)} item is unknown to this protocol snapshot.`, + severity: "error", + source: "openai", + }, + visibility: "owner_debug", + }, + ]; + } +} + +function toOpenAiToolStatus( + item: JsonObject, + phase: "completed" | "started", +): "cancelled" | "completed" | "failed" | null { + const itemType = readString(item, "type"); + const nativeStatus = readString(item, "status"); + + if (phase === "started") { + if (itemType === "collabAgentToolCall" && nativeStatus !== "inProgress") { + throw new Error( + `OpenAI ${itemType} started with non-running status ${String(nativeStatus)}.`, + ); + } + if (itemType === "imageGeneration" && nativeStatus !== "in_progress") { + throw new Error( + `OpenAI ${itemType} started with non-running status ${String(nativeStatus)}.`, + ); + } + return null; + } + + switch (itemType) { + case "commandExecution": + case "fileChange": + if (nativeStatus === "completed") { + return "completed"; + } + if (nativeStatus === "failed" || nativeStatus === "declined") { + return "failed"; + } + break; + case "mcpToolCall": + case "dynamicToolCall": + case "imageGeneration": + if (nativeStatus === "completed") { + return "completed"; + } + if (nativeStatus === "failed") { + return "failed"; + } + break; + case "collabAgentToolCall": + if (nativeStatus === "completed") { + return "completed"; + } + if (nativeStatus === "failed") { + return "failed"; + } + if (nativeStatus === "interrupted") { + return "cancelled"; + } + break; + default: + return null; + } + + throw new Error( + `OpenAI ${String(itemType)} completed with non-terminal status ${String(nativeStatus)}.`, + ); +} + export class OpenAiAppServerItemEventBridge { readonly #citationDiagnosticsEmitted = new Set(); readonly #citationFilters = new Map(); @@ -27,6 +329,7 @@ export class OpenAiAppServerItemEventBridge { readonly #messages: OpenAiMessageState; readonly #plans: OpenAiPlanState; readonly #push: OpenAiEventPush; + readonly #pushSession: OpenAiEventPush; readonly #tools: OpenAiToolState; constructor(input: { @@ -34,12 +337,14 @@ export class OpenAiAppServerItemEventBridge { messages: OpenAiMessageState; plans: OpenAiPlanState; push: OpenAiEventPush; + pushSession: OpenAiEventPush; tools: OpenAiToolState; }) { this.#items = input.items; this.#messages = input.messages; this.#plans = input.plans; this.#push = input.push; + this.#pushSession = input.pushSession; this.#tools = input.tools; } @@ -52,8 +357,8 @@ export class OpenAiAppServerItemEventBridge { this.#tools.reset(); } - finishOpen(): DriverEventInput[] { - return [...this.#messages.finishOpen(), ...this.#tools.failOpen()]; + terminalEvents(outcome: OpenAiTerminalOutcome): DriverEventInput[] { + return [...this.#messages.terminalEvents(outcome), ...this.#tools.terminalEvents(outcome)]; } async onMessageDelta(context: AgentDriverContext, params: JsonObject): Promise { @@ -109,35 +414,26 @@ export class OpenAiAppServerItemEventBridge { id: itemId, type: "fileChange", }; + const publicItemId = this.#items.publicId(itemId); const parentMessageId = this.#tools.parentMessage(itemId) ?? (await this.#messages.ensureTurnMessage(context, turnId, this.#push)); await this.#tools.ensureStarted(context, this.#push, { parentMessageId, + publicToolCallId: publicItemId, reason: "driver.openai.file_change.patch_updated.synthetic_start", + sourceScope: this.#items.publicId(turnId, "turn"), toolCallId: itemId, toolCallName: "File change", }); - const resultText = toOpenAiToolResultText(item); - const events: DriverEventInput[] = []; + const events = toOpenAiFileChangeEvents(item); - if (resultText !== null && resultText.length > 0) { - events.push({ - kind: "tool.call.updated", - payload: { - content: resultText, - messageId: parentMessageId, - rawOutput: resultText, - status: "running", - toolCallId: itemId, - }, - }); + for (const event of events) { + assertOpenAiDurableEventFits(event, `file change ${publicItemId}`); } - events.push(...toOpenAiFileChangeEvents(item)); - if (events.length > 0) { await this.#push(context, "driver.openai.file_change.patch_updated", events); } @@ -152,21 +448,66 @@ export class OpenAiAppServerItemEventBridge { return; } - if (!this.#items.markCompleted(itemId)) { + if (this.#items.isCompleted(itemId)) { return; } - const events: DriverEventInput[] = []; - await this.#appendMessageEnd(context, events, item, itemId, turnId); - this.#appendPlanEnd(events, item, itemId); - this.#appendReasoningEnd(events, item, itemId); - await this.#appendToolEnd(context, events, item, itemId, turnId); + // Validate and prepare the full completion before committing replay state. + const toolStatus = toOpenAiToolStatus(item, "completed"); + const publicItemId = this.#items.publicId(itemId); + const publicTurnId = this.#items.publicId(turnId, "turn"); + const lifecycleEvents = toOpenAiItemLifecycleEvents(item, publicItemId, "completed"); + const completions = await Promise.all([ + this.#prepareMessageEnd(context, item, itemId, publicItemId, publicTurnId, turnId), + this.#preparePlanEnd(item, itemId), + this.#prepareReasoningEnd(context, item, itemId), + this.#prepareToolEnd(context, item, itemId, publicItemId, publicTurnId, toolStatus, turnId), + ]); + const events = completions.flatMap((completion) => completion.events); events.push(...toOpenAiFileChangeEvents(item)); + events.push(...lifecycleEvents); if (events.length > 0) { - await this.#push(context, "driver.openai.item.completed", events); + const durableEvents = withOpenAiEventIds( + events, + `openai.item.completed:${publicTurnId}:${publicItemId}`, + ); + for (const event of durableEvents) { + assertOpenAiDurableEventFits(event, `item completion ${publicItemId}`); + } + await this.#push(context, "driver.openai.item.completed", durableEvents); + } + + for (const completion of completions) { + completion.commit(); } + this.#items.markCompleted(itemId); + } + + async onPostTerminalSubAgentActivity( + context: AgentDriverContext, + params: JsonObject, + ): Promise { + const item = readRecord(params, "item"); + const itemId = item === null ? null : readNonEmptyString(item, "id"); + const turnId = readNonEmptyString(params, "turnId"); + + if (item === null || itemId === null || turnId === null || this.#items.isCompleted(itemId)) { + return; + } + + const publicItemId = this.#items.publicId(itemId); + const publicTurnId = this.#items.publicId(turnId, "turn"); + const events = withOpenAiEventIds( + toOpenAiItemLifecycleEvents(item, publicItemId, "completed"), + `openai.item.completed:${publicTurnId}:${publicItemId}`, + ); + for (const event of events) { + assertOpenAiDurableEventFits(event, `post-terminal sub-agent activity ${publicItemId}`); + } + await this.#pushSession(context, "driver.openai.sub_agent.post_terminal", events); + this.#items.markCompleted(itemId); } async onItemStarted(context: AgentDriverContext, params: JsonObject): Promise { @@ -180,13 +521,34 @@ export class OpenAiAppServerItemEventBridge { const toolName = toOpenAiToolName(item); const itemId = readNonEmptyString(item, "id"); - if (toolName === null || itemId === null) { + if (itemId === null) { + return; + } + const publicItemId = this.#items.publicId(itemId); + const publicTurnId = this.#items.publicId(turnId, "turn"); + + if (toolName === null) { + const events = withOpenAiEventIds( + toOpenAiItemLifecycleEvents(item, publicItemId, "started"), + `openai.item.started:${publicTurnId}:${publicItemId}`, + ); + + if (events.length > 0) { + for (const event of events) { + assertOpenAiDurableEventFits(event, `item start ${publicItemId}`); + } + await this.#push(context, "driver.openai.item.started", events); + } return; } + toOpenAiToolStatus(item, "started"); + await this.#tools.ensureStarted(context, this.#push, { parentMessageId: await this.#messages.ensureTurnMessage(context, turnId, this.#push), + publicToolCallId: publicItemId, reason: "driver.openai.item.started", + sourceScope: publicTurnId, toolCallId: itemId, toolCallName: toolName, }); @@ -194,14 +556,24 @@ export class OpenAiAppServerItemEventBridge { async onPlanDelta(context: AgentDriverContext, params: JsonObject): Promise { const itemId = readNonEmptyString(params, "itemId"); + const turnId = readNonEmptyString(params, "turnId"); const delta = readString(params, "delta"); - if (itemId === null || delta === null || delta.length === 0) { + if (itemId === null || turnId === null || delta === null || delta.length === 0) { return; } - + const publicItemId = this.#items.publicId(itemId); + const publicTurnId = this.#items.publicId(turnId, "turn"); + + const event = { + ...this.#plans.createDeltaEvent(itemId, delta), + sourceEventId: `openai.plan.delta:${publicTurnId}:${publicItemId}:${String( + this.#plans.currentLength(itemId), + )}`, + }; + assertOpenAiDurableEventFits(event, `plan update ${publicItemId}`); + await this.#push(context, "driver.openai.plan.delta", [event]); this.#plans.appendDelta(itemId, delta); - await this.#push(context, "driver.openai.plan.delta", [this.#plans.createUpdatedEvent()]); } async onReasoningDelta(context: AgentDriverContext, params: JsonObject): Promise { @@ -212,35 +584,53 @@ export class OpenAiAppServerItemEventBridge { return; } - const messageId = `reasoning:${itemId}`; + const messageId = this.#messages.reasoningId(itemId); const events: DriverEventInput[] = []; if (this.#messages.isReasoningEnded(messageId)) { return; } - this.#messages.ensureReasoning(messageId, events); - events.push({ - delivery: "best_effort", - kind: "thought.delta", - payload: { - channel: "summary", - contentDelta: delta, - thoughtId: messageId, - }, - }); + const currentLength = this.#messages.currentReasoningText(messageId).length; + this.#messages.ensureReasoning(messageId, events, false); + events.push( + ...chunkOpenAiText(delta).map((contentDelta): DriverEventInput => ({ + delivery: "lossless", + kind: "thought.delta", + payload: { + channel: "summary", + contentDelta, + thoughtId: messageId, + }, + })), + ); - await this.#push(context, "driver.openai.reasoning.summary", events); + await this.#push( + context, + "driver.openai.reasoning.summary", + withOpenAiEventIds(events, `openai.reasoning.delta:${messageId}:${String(currentLength)}`), + ); + this.#messages.ensureReasoning(messageId, [], true); + this.#messages.appendReasoningText(messageId, delta); } async onReasoningPart(context: AgentDriverContext, params: JsonObject): Promise { const summaryIndex = params["summaryIndex"]; - if (typeof summaryIndex !== "number" || !Number.isInteger(summaryIndex) || summaryIndex < 1) { + const itemId = readNonEmptyString(params, "itemId"); + + if ( + itemId === null || + typeof summaryIndex !== "number" || + !Number.isInteger(summaryIndex) || + summaryIndex < 1 || + !this.#messages.beginReasoningPart(this.#messages.reasoningId(itemId), summaryIndex, false) + ) { return; } await this.onReasoningDelta(context, { ...params, delta: "\n\n" }); + this.#messages.beginReasoningPart(this.#messages.reasoningId(itemId), summaryIndex, true); } async onToolOutput(context: AgentDriverContext, params: JsonObject): Promise { @@ -252,8 +642,9 @@ export class OpenAiAppServerItemEventBridge { } const parentMessageId = this.#tools.parentMessage(itemId); + const publicToolCallId = this.#tools.publicToolCallId(itemId); - if (parentMessageId === null) { + if (parentMessageId === null || publicToolCallId === null) { return; } @@ -262,11 +653,10 @@ export class OpenAiAppServerItemEventBridge { delivery: "best_effort", kind: "tool.call.updated", payload: { - content: delta, messageId: parentMessageId, rawOutput: delta, status: "running", - toolCallId: itemId, + toolCallId: publicToolCallId, }, }, ]); @@ -306,9 +696,16 @@ export class OpenAiAppServerItemEventBridge { const items = readArray(turn, "items"); const itemsView = readString(turn, "itemsView"); - const finalAssistantItem = items.findLast( - (item) => isRecord(item) && readString(item, "type") === "agentMessage", + const assistantItems = items.filter( + (item): item is JsonObject => + isRecord(item) && + readString(item, "type") === "agentMessage" && + readString(item, "delivery") !== "async", ); + const hasExplicitPhase = assistantItems.some((item) => toOpenAiMessagePhase(item) !== null); + const finalAssistantItem = hasExplicitPhase + ? assistantItems.findLast((item) => toOpenAiMessagePhase(item) === "final") + : assistantItems.at(-1); if (!isRecord(finalAssistantItem)) { // Terminal notifications commonly use `itemsView: "notLoaded"` with an @@ -344,13 +741,46 @@ export class OpenAiAppServerItemEventBridge { this.#push, ); const filteredText = filterOpenAiPrivateCitations(text); + const phase = toOpenAiMessagePhase(finalAssistantItem); + const publicItemId = this.#items.publicId(itemId); + const publicTurnId = this.#items.publicId(turnId, "turn"); await this.#reportCitations(context, messageId, filteredText.privateCitationCount); - this.#messages.setText(messageId, filteredText.text); + const snapshot = { + itemId, + messageId, + phase, + text: filteredText.text, + turnId, + }; + + if (this.#messages.needsSnapshot(snapshot)) { + await this.#push( + context, + "driver.openai.turn.final_message", + withOpenAiEventIds( + toOpenAiMessageSnapshotEvents({ + item: finalAssistantItem, + itemId, + messageId, + phase, + sourcePrefix: `openai.turn.final_message:${publicTurnId}:${publicItemId}`, + text: filteredText.text, + }), + `openai.turn.final_message:${publicTurnId}:${publicItemId}`, + ), + ); + this.#messages.setText(messageId, filteredText.text); + this.#messages.recordSnapshot(snapshot); + } this.#releaseCitationState(messageId); return { id: messageId, text: filteredText.text }; } async onTurnPlan(context: AgentDriverContext, params: JsonObject): Promise { + const turnId = readNonEmptyString(params, "turnId"); + if (turnId === null) { + return; + } const plan = readArray(params, "plan").flatMap((entry) => { if (!isRecord(entry)) { return []; @@ -371,42 +801,48 @@ export class OpenAiAppServerItemEventBridge { ]; }); - await this.#push(context, "driver.openai.turn.plan.updated", [ - { - kind: "plan.updated", - payload: { - entries: plan, - source: "driver", - }, + const event: DriverEventInput = { + kind: "plan.updated", + payload: { + entries: plan, + source: "driver", }, - ]); + sourceEventId: `openai.turn.plan:${this.#items.publicId(turnId, "turn")}:${String( + this.#plans.turnVersion(turnId), + )}`, + }; + assertOpenAiDurableEventFits(event, "turn plan update"); + await this.#push(context, "driver.openai.turn.plan.updated", [event]); + this.#plans.advanceTurnVersion(turnId); } - async #appendMessageEnd( + async #prepareMessageEnd( context: AgentDriverContext, - events: DriverEventInput[], item: JsonObject, itemId: string, + publicItemId: string, + publicTurnId: string, turnId: string, - ): Promise { + ): Promise { if (readString(item, "type") !== "agentMessage") { - return; + return { commit: () => {}, events: [] }; } + const events: DriverEventInput[] = []; const finalText = readString(item, "text"); + const isAsync = readString(item, "delivery") === "async"; const messageId = await this.#messages.ensureItemMessage( context, { itemId, turnId }, this.#push, ); const filteredFinalText = finalText === null ? null : filterOpenAiPrivateCitations(finalText); - const currentText = this.#messages.currentText(messageId); + let trailingText = ""; if (filteredFinalText === null) { - const trailingText = this.#citationFilters.get(messageId)?.finish().text ?? ""; + trailingText = this.#citationFilters.get(messageId)?.previewFinish().text ?? ""; if (trailingText.length > 0) { - this.#messages.appendText(messageId, trailingText); events.push({ delivery: "best_effort", kind: "message.delta", @@ -419,68 +855,25 @@ export class OpenAiAppServerItemEventBridge { } } - if (filteredFinalText !== null) { - this.#appendCitationDiag(events, messageId, filteredFinalText.privateCitationCount); - } - - if (filteredFinalText !== null && filteredFinalText.text.length > currentText.length) { - if (filteredFinalText.text.startsWith(currentText)) { - const delta = filteredFinalText.text.slice(currentText.length); - this.#messages.appendText(messageId, delta); - events.push({ - delivery: "best_effort", - kind: "message.delta", - payload: { - contentDelta: delta, - messageId, - role: "agent", - }, - }); - } else if (currentText.length === 0) { - this.#messages.appendText(messageId, filteredFinalText.text); - events.push({ - delivery: "best_effort", - kind: "message.delta", - payload: { - contentDelta: filteredFinalText.text, - messageId, - role: "agent", - }, - }); - } else { - context.logger.warn("driver.openai.agent.final_text.mismatch", { - currentLength: currentText.length, - finalLength: filteredFinalText.text.length, - itemId, - }); - } - } + const commitCitationDiagnostic = + filteredFinalText !== null + ? this.#appendCitationDiag(events, messageId, filteredFinalText.privateCitationCount) + : false; if (filteredFinalText !== null) { - // item/completed is the provider's authoritative snapshot. Streaming - // deltas may be missing or replayed, so canonical completion must not - // inherit a corrupted accumulator even when live deltas cannot be undone. - this.#messages.setText(messageId, filteredFinalText.text); - this.#messages.recordSnapshot({ - itemId, - messageId, - text: filteredFinalText.text, - turnId, - }); - events.push({ - delivery: "lossless", - kind: "message.added", - payload: { - content: filteredFinalText.text, + events.push( + ...toOpenAiMessageSnapshotEvents({ + item, + itemId, messageId, - role: "agent", - }, - }); + phase: toOpenAiMessagePhase(item), + sourcePrefix: `openai.item.completed:${publicTurnId}:${publicItemId}`, + text: filteredFinalText.text, + }), + ); } - this.#citationFilters.delete(messageId); - - if (this.#messages.markEnded(messageId)) { + if (!this.#messages.isEnded(messageId)) { events.push({ kind: "message.completed", payload: { @@ -489,18 +882,46 @@ export class OpenAiAppServerItemEventBridge { }, }); } + + return { + commit: () => { + if (commitCitationDiagnostic) { + this.#citationDiagnosticsEmitted.add(messageId); + } + if (filteredFinalText === null) { + this.#citationFilters.get(messageId)?.finish(); + this.#messages.appendText(messageId, trailingText); + } else { + // item/completed is the provider's authoritative snapshot. Streaming + // deltas may be missing or replayed, so canonical completion must not + // inherit a corrupted accumulator even when live deltas cannot be undone. + this.#messages.setText(messageId, filteredFinalText.text); + if (!isAsync) { + this.#messages.recordSnapshot({ + itemId, + messageId, + phase: toOpenAiMessagePhase(item), + text: filteredFinalText.text, + turnId, + }); + } + } + this.#citationFilters.delete(messageId); + this.#messages.markEnded(messageId); + }, + events, + }; } #appendCitationDiag( events: DriverEventInput[], messageId: string, privateCitationCount: number, - ): void { + ): boolean { if (privateCitationCount === 0 || this.#citationDiagnosticsEmitted.has(messageId)) { - return; + return false; } - this.#citationDiagnosticsEmitted.add(messageId); events.push({ kind: "diagnostic.reported", payload: { @@ -514,6 +935,7 @@ export class OpenAiAppServerItemEventBridge { }, visibility: "owner_debug", }); + return true; } #filterCitationDelta(messageId: string, delta: string): string { @@ -533,11 +955,14 @@ export class OpenAiAppServerItemEventBridge { privateCitationCount: number, ): Promise { const events: DriverEventInput[] = []; - this.#appendCitationDiag(events, messageId, privateCitationCount); + const commit = this.#appendCitationDiag(events, messageId, privateCitationCount); if (events.length > 0) { await this.#push(context, "driver.openai.private_citation_markup_removed", events); } + if (commit) { + this.#citationDiagnosticsEmitted.add(messageId); + } } #releaseCitationState(messageId: string): void { @@ -545,62 +970,108 @@ export class OpenAiAppServerItemEventBridge { this.#citationFilters.delete(messageId); } - #appendPlanEnd(events: DriverEventInput[], item: JsonObject, itemId: string): void { + #preparePlanEnd(item: JsonObject, itemId: string): OpenAiItemCompletionCommit { if (readString(item, "type") !== "plan") { - return; + return { commit: () => {}, events: [] }; } const planText = readString(item, "text"); if (planText === null || planText.trim().length === 0) { - return; + return { commit: () => {}, events: [] }; } - this.#plans.setCompleted(itemId, planText); - events.push(this.#plans.createUpdatedEvent()); + return { + commit: () => this.#plans.setCompleted(itemId, planText), + events: [this.#plans.createCompletedEvent(itemId, planText)], + }; } - #appendReasoningEnd(events: DriverEventInput[], item: JsonObject, itemId: string): void { + #prepareReasoningEnd( + context: AgentDriverContext, + item: JsonObject, + itemId: string, + ): OpenAiItemCompletionCommit { if (readString(item, "type") !== "reasoning") { - return; + return { commit: () => {}, events: [] }; } + const events: DriverEventInput[] = []; const summary = Array.isArray(item["summary"]) ? item["summary"].filter((entry): entry is string => typeof entry === "string") : []; - const messageId = `reasoning:${itemId}`; - - if (summary.length > 0) { - this.#messages.ensureReasoning(messageId, events); - events.push({ - delivery: "best_effort", - kind: "thought.delta", - payload: { - channel: "summary", - contentDelta: summary.join("\n\n"), - thoughtId: messageId, - }, + const messageId = this.#messages.reasoningId(itemId); + const summaryText = summary.join("\n\n"); + const currentText = this.#messages.currentReasoningText(messageId); + const missingText = summaryText.startsWith(currentText) + ? summaryText.slice(currentText.length) + : currentText.length === 0 + ? summaryText + : ""; + + if ( + summaryText.length > 0 && + currentText.length > 0 && + summaryText !== currentText && + missingText.length === 0 + ) { + context.logger.warn("driver.openai.reasoning.final_text.mismatch", { + currentLength: currentText.length, + finalLength: summaryText.length, + itemId, }); } - if (this.#messages.markReasoningEnded(messageId)) { + if (missingText.length > 0) { + this.#messages.ensureReasoning(messageId, events, false); + events.push( + ...chunkOpenAiText(missingText).map((contentDelta, index): DriverEventInput => ({ + delivery: "lossless", + kind: "thought.delta", + payload: { + channel: "summary", + contentDelta, + thoughtId: messageId, + }, + sourceEventId: `openai.reasoning.completed:${messageId}:${String(index)}`, + })), + ); + } + + const shouldStart = missingText.length > 0 && !this.#messages.isReasoningStarted(messageId); + if (this.#messages.isReasoningStarted(messageId) || shouldStart) { events.push({ kind: "thought.completed", payload: { channel: "summary", thoughtId: messageId, }, + sourceEventId: `openai.reasoning.completed:${messageId}:terminal`, }); } + + return { + commit: () => { + if (shouldStart) { + this.#messages.ensureReasoning(messageId, [], true); + } + this.#messages.appendReasoningText(messageId, missingText); + this.#messages.markReasoningEnded(messageId); + }, + events, + }; } - async #appendToolEnd( + async #prepareToolEnd( context: AgentDriverContext, - events: DriverEventInput[], item: JsonObject, itemId: string, + publicItemId: string, + publicTurnId: string, + status: "cancelled" | "completed" | "failed" | null, turnId: string, - ): Promise { + ): Promise { + const events: DriverEventInput[] = []; const toolName = toOpenAiToolName(item); const parentMessageId = this.#tools.parentMessage(itemId) ?? @@ -610,44 +1081,72 @@ export class OpenAiAppServerItemEventBridge { : await this.#messages.ensureTurnMessage(context, turnId, this.#push)); if (parentMessageId === null || toolName === null) { - return; + return { commit: () => {}, events }; } await this.#tools.ensureStarted(context, this.#push, { parentMessageId, + publicToolCallId: publicItemId, reason: "driver.openai.item.completed.synthetic_start", + sourceScope: publicTurnId, toolCallId: itemId, toolCallName: toolName, }); - const nativeStatus = readString(item, "status"); - const status = - nativeStatus === "failed" || nativeStatus === "declined" ? "failed" : "completed"; + const itemType = readString(item, "type"); + const terminalStatus = status ?? "completed"; const toolResult = toOpenAiToolResultText(item); + const rawInput = toOpenAiToolRawInput(item); + const collaborationOutput = toOpenAiCollaborationOutput(item); + const receiverThreadIds = readArray(item, "receiverThreadIds").filter( + (entry): entry is string => typeof entry === "string", + ); + const structuredOutput = + toOpenAiToolStructuredOutput(item) ?? + collaborationOutput ?? + (itemType === "webSearch" + ? { + action: item["action"] ?? null, + query: readString(item, "query"), + results: item["results"] ?? null, + } + : null); events.push({ kind: "tool.call.updated", payload: { - ...(toolResult === null || toolResult.length === 0 + ...(itemType === "collabAgentToolCall" && receiverThreadIds.length === 1 + ? { agentId: receiverThreadIds[0] } + : {}), + ...(toolResult === null || + toolResult.length === 0 || + ((itemType === "dynamicToolCall" || itemType === "collabAgentToolCall") && + structuredOutput !== null) ? {} : { - content: toolResult, messageId: parentMessageId, rawOutput: toolResult, }), - status, - toolCallId: itemId, + ...(structuredOutput === null ? {} : { structuredOutput }), + ...(rawInput === null ? {} : { rawInput }), + status: terminalStatus, + toolCallId: publicItemId, }, + sourceEventId: `openai.item.completed:${publicTurnId}:${publicItemId}:0`, }); + assertOpenAiDurableEventFits(events[0]!, `tool completion ${publicItemId}`); events.push({ kind: "item.completed", payload: { - itemId, + itemId: publicItemId, itemType: "tool_call", - status, + status: terminalStatus, }, }); - this.#tools.markEnded(itemId); + return { + commit: () => this.#tools.markEnded(itemId), + events, + }; } } diff --git a/src/runtimes/openai/app-server-json.ts b/src/runtimes/openai/app-server-json.ts index 28181cb..723295a 100644 --- a/src/runtimes/openai/app-server-json.ts +++ b/src/runtimes/openai/app-server-json.ts @@ -1,3 +1,6 @@ +import { isRecord } from "../provider-json"; +import type { JsonObject } from "../provider-json"; + export { isRecord, readArray, @@ -9,6 +12,17 @@ export { export type { JsonObject } from "../provider-json"; export type JsonRpcId = number | string; +export function toGrantedPermissionProfile(value: unknown): JsonObject { + if (!isRecord(value)) { + return {}; + } + + return { + ...(isRecord(value["fileSystem"]) ? { fileSystem: value["fileSystem"] } : {}), + ...(isRecord(value["network"]) ? { network: value["network"] } : {}), + }; +} + export function toJsonRpcId(value: unknown): JsonRpcId | null { if (typeof value === "string" || typeof value === "number") { return value; diff --git a/src/runtimes/openai/app-server-protocol-client-schemas.ts b/src/runtimes/openai/app-server-protocol-client-schemas.ts new file mode 100644 index 0000000..3de3e08 --- /dev/null +++ b/src/runtimes/openai/app-server-protocol-client-schemas.ts @@ -0,0 +1,69 @@ +import { z } from "zod"; + +import initializeResponseJsonSchema from "./generated-json-schema/InitializeResponse.json" with { type: "json" }; +import threadBackgroundTerminalsCleanResponseJsonSchema from "./generated-json-schema/ThreadBackgroundTerminalsCleanResponse.json" with { type: "json" }; +import threadInjectItemsResponseJsonSchema from "./generated-json-schema/ThreadInjectItemsResponse.json" with { type: "json" }; +import threadResumeResponseJsonSchema from "./generated-json-schema/ThreadResumeResponse.json" with { type: "json" }; +import threadStartResponseJsonSchema from "./generated-json-schema/ThreadStartResponse.json" with { type: "json" }; +import turnStartResponseJsonSchema from "./generated-json-schema/TurnStartResponse.json" with { type: "json" }; +import type { + InitializeResponse, + ThreadBackgroundTerminalsCleanResponse, + ThreadInjectItemsResponse, + ThreadResumeResponse, + ThreadStartResponse, + TurnStartResponse, +} from "./app-server-protocol-types"; +import { validateTurnStatusError } from "./app-server-turn-validation"; + +function fromGeneratedJsonSchema(schema: unknown): z.ZodType { + return z.fromJSONSchema(schema as Parameters[0]) as z.ZodType; +} + +const requestIdSchema = z.union([z.string(), z.number().int().safe()]); + +export const jsonRpcResponseSchema = z.union([ + z.strictObject({ + id: requestIdSchema, + result: z.json(), + }), + z.strictObject({ + error: z.strictObject({ + code: z.number().int().safe(), + data: z.json().optional(), + message: z.string(), + }), + id: requestIdSchema, + }), +]); + +export const initializeResponseSchema = fromGeneratedJsonSchema( + initializeResponseJsonSchema, +); +export const threadBackgroundTerminalsCleanResponseSchema = + fromGeneratedJsonSchema( + threadBackgroundTerminalsCleanResponseJsonSchema, + ); +export const threadInjectItemsResponseSchema = fromGeneratedJsonSchema( + threadInjectItemsResponseJsonSchema, +); +export const threadResumeResponseSchema = fromGeneratedJsonSchema( + threadResumeResponseJsonSchema, +); +export const threadStartResponseSchema = fromGeneratedJsonSchema( + threadStartResponseJsonSchema, +); +export const turnStartResponseSchema = fromGeneratedJsonSchema( + turnStartResponseJsonSchema, +).superRefine((response, context) => { + const message = validateTurnStatusError(response.turn, false); + + if (message !== null) { + context.addIssue({ + code: "custom", + input: response.turn, + message, + path: ["turn", "error"], + }); + } +}); diff --git a/src/runtimes/openai/app-server-protocol-client.ts b/src/runtimes/openai/app-server-protocol-client.ts new file mode 100644 index 0000000..bcf516b --- /dev/null +++ b/src/runtimes/openai/app-server-protocol-client.ts @@ -0,0 +1,21 @@ +import { + initializeResponseSchema, + threadBackgroundTerminalsCleanResponseSchema, + threadInjectItemsResponseSchema, + threadResumeResponseSchema, + threadStartResponseSchema, + turnStartResponseSchema, +} from "./app-server-protocol-client-schemas"; +import type { ClientRequestMethod, ClientRequestResult } from "./app-server-protocol-types"; + +export const CLIENT_REQUEST_RESULT_PARSERS: { + [Method in ClientRequestMethod]: (value: unknown) => ClientRequestResult[Method]; +} = { + initialize: (value) => initializeResponseSchema.parse(value), + "thread/backgroundTerminals/clean": (value) => + threadBackgroundTerminalsCleanResponseSchema.parse(value), + "thread/inject_items": (value) => threadInjectItemsResponseSchema.parse(value), + "thread/resume": (value) => threadResumeResponseSchema.parse(value), + "thread/start": (value) => threadStartResponseSchema.parse(value), + "turn/start": (value) => turnStartResponseSchema.parse(value), +}; diff --git a/src/runtimes/openai/app-server-protocol-server.ts b/src/runtimes/openai/app-server-protocol-server.ts new file mode 100644 index 0000000..a7952a5 --- /dev/null +++ b/src/runtimes/openai/app-server-protocol-server.ts @@ -0,0 +1,153 @@ +import { z } from "zod"; + +import serverNotificationJsonSchema from "./generated-json-schema/ServerNotification.json" with { type: "json" }; +import serverRequestJsonSchema from "./generated-json-schema/ServerRequest.json" with { type: "json" }; +import { isRecord } from "./app-server-json"; +import type { JsonObject } from "./app-server-json"; +import type { + ParsedServerNotification, + ParsedServerRequest, + ServerNotificationMethod, + ServerRequestMethod, +} from "./app-server-protocol-types"; +import { validateTurnStatusError } from "./app-server-turn-validation"; + +interface MethodDispatcher { + has(method: string): method is Method; + parse(method: Method, value: unknown): JsonObject; +} + +function expectObject(value: unknown, label: string): JsonObject { + if (!isRecord(value)) { + throw new TypeError(`${label} must be an object.`); + } + + return value; +} + +function readStringArray(value: unknown, label: string): string[] { + if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) { + throw new TypeError(`${label} must be an array of strings.`); + } + + return value; +} + +function branchMethod(branch: JsonObject): string { + const properties = expectObject(branch["properties"], "Method schema properties"); + const method = expectObject(properties["method"], "Method schema discriminator"); + const variants = readStringArray(method["enum"], "Method schema discriminator enum"); + + if (variants.length !== 1) { + throw new TypeError("Method schema discriminator must contain exactly one value."); + } + + return variants[0]!; +} + +function createMethodDispatcher(value: unknown): MethodDispatcher { + const root = expectObject(value, "Root method schema"); + const branches = root["oneOf"]; + + if (!Array.isArray(branches)) { + throw new TypeError("Root method schema oneOf must be an array."); + } + + const { oneOf: _oneOf, ...shared } = root; + const sharedProperties = isRecord(shared["properties"]) ? shared["properties"] : {}; + const sharedRequired = + shared["required"] === undefined + ? [] + : readStringArray(shared["required"], "Root method schema required"); + const schemas = new Map>(); + + for (const value of branches) { + const branch = expectObject(value, "Method schema branch"); + const method = branchMethod(branch); + const properties = expectObject(branch["properties"], `${method} schema properties`); + const required = readStringArray(branch["required"], `${method} schema required`); + const schema = z.fromJSONSchema({ + ...shared, + ...branch, + properties: { ...sharedProperties, ...properties }, + required: [...new Set([...sharedRequired, ...required])], + } as Parameters[0]) as z.ZodType; + + if (schemas.has(method)) { + throw new TypeError(`Root method schema contains duplicate method ${method}.`); + } + + schemas.set(method, schema); + } + + return { + has: (method: string): method is Method => schemas.has(method), + parse: (method, input) => schemas.get(method)!.parse(input), + }; +} + +const serverNotificationDispatcher = createMethodDispatcher( + serverNotificationJsonSchema, +); +const serverRequestDispatcher = + createMethodDispatcher(serverRequestJsonSchema); + +export function isServerNotificationMethod(method: string): method is ServerNotificationMethod { + return serverNotificationDispatcher.has(method); +} + +export function isServerRequestMethod(method: string): method is ServerRequestMethod { + return serverRequestDispatcher.has(method); +} + +export function parseServerNotification(value: unknown): ParsedServerNotification | null { + const envelope = isRecord(value) ? value : null; + const method = envelope?.["method"]; + + if (typeof method !== "string" || !serverNotificationDispatcher.has(method)) { + return null; + } + + const parsed = serverNotificationDispatcher.parse(method, value); + const params = parsed["params"]; + + if (!isRecord(params)) { + throw new TypeError(`${method} params must be an object.`); + } + + if (method === "turn/completed") { + const turn = expectObject(params["turn"], "turn/completed turn"); + const message = validateTurnStatusError(turn, true); + + if (message !== null) { + throw new TypeError(`turn/completed ${message}`); + } + } + + const emittedAtMs = parsed["emittedAtMs"]; + + return { + ...(typeof emittedAtMs === "number" ? { emittedAtMs } : {}), + method, + params, + }; +} + +export function parseServerRequest(value: unknown): ParsedServerRequest | null { + const envelope = isRecord(value) ? value : null; + const method = envelope?.["method"]; + + if (typeof method !== "string" || !serverRequestDispatcher.has(method)) { + return null; + } + + const parsed = serverRequestDispatcher.parse(method, value); + const id = parsed["id"]; + const params = parsed["params"]; + + if ((typeof id !== "number" && typeof id !== "string") || !isRecord(params)) { + throw new TypeError(`${method} request envelope is invalid.`); + } + + return { id, method, params }; +} diff --git a/src/runtimes/openai/app-server-protocol-types.ts b/src/runtimes/openai/app-server-protocol-types.ts new file mode 100644 index 0000000..d4c99d7 --- /dev/null +++ b/src/runtimes/openai/app-server-protocol-types.ts @@ -0,0 +1,92 @@ +import type { + ClientRequest as GeneratedClientRequest, + InitializeParams as GeneratedInitializeParams, + RequestId as GeneratedRequestId, + ServerNotification as GeneratedServerNotification, + ServerRequest as GeneratedServerRequest, +} from "./generated"; +import type { + AskForApproval, + CurrentTimeReadResponse as GeneratedCurrentTimeReadResponse, + SandboxPolicy as GeneratedSandboxPolicy, + ThreadInjectItemsParams as GeneratedThreadInjectItemsParams, + ThreadResumeParams as GeneratedThreadResumeParams, + ThreadStartParams as GeneratedThreadStartParams, + TurnStartParams as GeneratedTurnStartParams, + TurnStatus as GeneratedTurnStatus, + UserInput as GeneratedUserInput, +} from "./generated/v2"; +import type { JsonObject } from "./app-server-json"; + +type WireServerNotification = Exclude< + GeneratedServerNotification, + { method: "rawResponse/completed" | "rawResponseItem/completed" } +>; + +export type RequestId = GeneratedRequestId; +export type ApprovalPolicy = AskForApproval; +export type SandboxPolicy = GeneratedSandboxPolicy; +export type UserInput = GeneratedUserInput; + +export type InitializeParams = GeneratedInitializeParams; +export type ThreadStartParams = GeneratedThreadStartParams; +export type ThreadResumeParams = GeneratedThreadResumeParams; +export type ThreadInjectItemsParams = GeneratedThreadInjectItemsParams; +export type TurnStartParams = GeneratedTurnStartParams; +export type TurnStatus = GeneratedTurnStatus; + +export type InitializeResponse = JsonObject; +export type ThreadStartResponse = JsonObject & { thread: JsonObject & { id: string } }; +export type ThreadResumeResponse = ThreadStartResponse; +export type ThreadInjectItemsResponse = JsonObject; +export type ThreadBackgroundTerminalsCleanResponse = JsonObject; +export type TurnStartResponse = JsonObject & { + turn: JsonObject & { id: string; status: TurnStatus }; +}; + +export interface ClientRequestResult { + initialize: InitializeResponse; + "thread/backgroundTerminals/clean": ThreadBackgroundTerminalsCleanResponse; + "thread/inject_items": ThreadInjectItemsResponse; + "thread/resume": ThreadResumeResponse; + "thread/start": ThreadStartResponse; + "turn/start": TurnStartResponse; +} + +export type ClientRequestMethod = keyof ClientRequestResult; + +type ClientRequestParamsFor = + Extract extends { params: infer Params } + ? Params + : never; + +export type ClientRequestParams = { + [Method in ClientRequestMethod]: ClientRequestParamsFor; +}; + +export type ServerNotificationMethod = WireServerNotification["method"]; +export type ServerNotificationParams = { + [Method in ServerNotificationMethod]: JsonObject; +}; + +export interface ParsedServerNotification { + emittedAtMs?: number; + method: ServerNotificationMethod; + params: JsonObject; +} + +export type ServerRequestMethod = GeneratedServerRequest["method"]; + +export interface ParsedServerRequest { + id: RequestId; + method: ServerRequestMethod; + params: JsonObject; +} + +export interface PermissionsRequestApprovalResponse { + permissions: JsonObject; + scope: "turn" | "session"; + strictAutoReview?: boolean; +} + +export type CurrentTimeReadResponse = GeneratedCurrentTimeReadResponse; diff --git a/src/runtimes/openai/generated/app-server-protocol.ts b/src/runtimes/openai/app-server-protocol.ts similarity index 100% rename from src/runtimes/openai/generated/app-server-protocol.ts rename to src/runtimes/openai/app-server-protocol.ts diff --git a/src/runtimes/openai/app-server-request-handler.ts b/src/runtimes/openai/app-server-request-handler.ts index bfdc499..a7d70a7 100644 --- a/src/runtimes/openai/app-server-request-handler.ts +++ b/src/runtimes/openai/app-server-request-handler.ts @@ -1,20 +1,25 @@ import { PermissionEventDeliveryError } from "../../core/driver-permission-broker"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; -import { isRecord, readRecord, readString, stringifyForDisplay } from "./app-server-json"; +import { + isRecord, + readRecord, + readString, + stringifyForDisplay, + toGrantedPermissionProfile, +} from "./app-server-json"; import type { JsonObject } from "./app-server-json"; import type { - CommandExecutionRequestApprovalResponse, CurrentTimeReadResponse, - FileChangeRequestApprovalResponse, PermissionsRequestApprovalResponse, RequestId, ServerRequestMethod, -} from "./generated/app-server-protocol"; +} from "./app-server-protocol"; interface OpenAiAppServerRequestHandlerOptions { readonly context: AgentDriverContext; readonly handleError: (error: Error, method: ServerRequestMethod) => Promise; readonly isStopped: () => boolean; + readonly mapToolCallId: (toolCallId: string) => string; readonly respond: (id: RequestId, result: unknown) => void; readonly respondError: (id: RequestId, message: string) => void; } @@ -26,15 +31,53 @@ interface PendingServerRequest { function toApprovalDecision( decision: "allow_once" | "reject_once", -): CommandExecutionRequestApprovalResponse["decision"] { - return decision === "allow_once" ? "accept" : "decline"; + method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval", + params: JsonObject, +): "accept" | "cancel" | "decline" { + const explicit = params["availableDecisions"]; + + if ( + method === "item/commandExecution/requestApproval" && + explicit !== undefined && + explicit !== null && + !Array.isArray(explicit) + ) { + throw new TypeError("OpenAi approval availableDecisions must be an array or null."); + } + + const available = new Set( + (method === "item/commandExecution/requestApproval" && Array.isArray(explicit) + ? explicit + : method === "item/fileChange/requestApproval" + ? ["accept", "decline", "cancel"] + : ["accept", "cancel"] + ).filter( + (value): value is "accept" | "cancel" | "decline" => + value === "accept" || value === "cancel" || value === "decline", + ), + ); + const desired = decision === "allow_once" ? "accept" : "decline"; + + if (available.has(desired)) { + return desired; + } + + if (available.has("cancel")) { + return "cancel"; + } + + if (available.has("decline")) { + return "decline"; + } + + throw new Error(`OpenAi approval decision ${desired} was not offered by app-server.`); } function toPermissionProfileGrant(params: JsonObject): PermissionsRequestApprovalResponse { const permissions = readRecord(params, "permissions"); return { - permissions: permissions === null ? {} : { ...permissions }, + permissions: toGrantedPermissionProfile(permissions), scope: "turn", }; } @@ -47,6 +90,7 @@ export class OpenAiAppServerRequestHandler { readonly #context: AgentDriverContext; readonly #handleError: OpenAiAppServerRequestHandlerOptions["handleError"]; readonly #isStopped: () => boolean; + readonly #mapToolCallId: OpenAiAppServerRequestHandlerOptions["mapToolCallId"]; readonly #pending = new Map(); readonly #respond: OpenAiAppServerRequestHandlerOptions["respond"]; readonly #respondError: OpenAiAppServerRequestHandlerOptions["respondError"]; @@ -55,10 +99,15 @@ export class OpenAiAppServerRequestHandler { this.#context = options.context; this.#handleError = options.handleError; this.#isStopped = options.isStopped; + this.#mapToolCallId = options.mapToolCallId; this.#respond = options.respond; this.#respondError = options.respondError; } + isPending(id: RequestId): boolean { + return this.#pending.has(id); + } + dispatch(method: ServerRequestMethod, id: RequestId, params: unknown): void { if (this.#pending.has(id)) { throw new Error(`OpenAi app-server request ${String(id)} is already pending.`); @@ -135,21 +184,21 @@ export class OpenAiAppServerRequestHandler { case "item/fileChange/requestApproval": { const decision = await this.#context.ports.permission.request( { - rawInput: stringifyForDisplay(payload["command"] ?? payload["reason"] ?? payload), + rawInput: stringifyForDisplay(payload), requestId, title: method === "item/fileChange/requestApproval" ? "Approve file changes" - : "Approve command execution", - toolCallId: readString(payload, "itemId"), + : readString(payload, "kind") === "writeStdin" + ? "Approve terminal input" + : "Approve command execution", + toolCallId: this.#publicToolCallId(payload), toolKind: method, }, signal, ); - const response: - | CommandExecutionRequestApprovalResponse - | FileChangeRequestApprovalResponse = { - decision: toApprovalDecision(decision), + const response = { + decision: toApprovalDecision(decision, method, payload), }; this.#reply(id, response); return; @@ -157,10 +206,10 @@ export class OpenAiAppServerRequestHandler { case "item/permissions/requestApproval": { const decision = await this.#context.ports.permission.request( { - rawInput: stringifyForDisplay(payload["permissions"] ?? payload), + rawInput: stringifyForDisplay(payload), requestId, title: "Approve runtime permissions", - toolCallId: readString(payload, "itemId"), + toolCallId: this.#publicToolCallId(payload), toolKind: method, }, signal, @@ -173,8 +222,10 @@ export class OpenAiAppServerRequestHandler { ); return; } + case "applyPatchApproval": case "account/chatgptAuthTokens/refresh": case "attestation/generate": + case "execCommandApproval": case "item/tool/call": case "item/tool/requestUserInput": case "mcpServer/elicitation/request": @@ -191,6 +242,11 @@ export class OpenAiAppServerRequestHandler { } } + #publicToolCallId(payload: JsonObject): string | null { + const toolCallId = readString(payload, "itemId"); + return toolCallId === null ? null : this.#mapToolCallId(toolCallId); + } + #replyError(id: RequestId, message: string): void { if (this.#pending.delete(id) && !this.#isStopped()) { this.#respondError(id, message); diff --git a/src/runtimes/openai/app-server-turn-tracker.ts b/src/runtimes/openai/app-server-turn-tracker.ts index ce1221b..868ba98 100644 --- a/src/runtimes/openai/app-server-turn-tracker.ts +++ b/src/runtimes/openai/app-server-turn-tracker.ts @@ -1,7 +1,10 @@ +import { createHash } from "node:crypto"; + import type { DriverTurnCancelledError } from "../../core/driver-runtime-state"; import type { RunId } from "../../protocol/id"; interface ActiveOpenAiTurn { + nativeTurnId: string; promise: Promise; reject(error: Error): void; resolve(): void; @@ -13,10 +16,18 @@ type TerminalOpenAiTurn = | { error: DriverTurnCancelledError | Error; kind: "failed" }; const MAX_RETAINED_TURNS = 1_024; +const MAX_RETAINED_NATIVE_TURN_ID_BYTES = 256; + +export function retainedOpenAiTurnKey(turnId: string): string { + return Buffer.byteLength(turnId, "utf8") <= MAX_RETAINED_NATIVE_TURN_ID_BYTES + ? turnId + : `sha256:${createHash("sha256").update(turnId).digest("hex")}`; +} function rememberBounded(map: Map, turnId: string, value: T): void { - map.delete(turnId); - map.set(turnId, value); + const key = retainedOpenAiTurnKey(turnId); + map.delete(key); + map.set(key, value); if (map.size > MAX_RETAINED_TURNS) { const oldest = map.keys().next().value; @@ -38,11 +49,11 @@ export class OpenAiTurnTracker { return null; } - return this.#activeTurns.get(turnId)?.runId ?? null; + return this.#activeTurns.get(retainedOpenAiTurnKey(turnId))?.runId ?? null; } activeTurnIds(): string[] { - return [...this.#activeTurns.keys()]; + return [...this.#activeTurns.values()].map(({ nativeTurnId }) => nativeTurnId); } clearActiveTurns(): void { @@ -53,11 +64,12 @@ export class OpenAiTurnTracker { } hasTerminal(turnId: string): boolean { - return this.#settlingTurnIds.has(turnId) || this.#terminalTurns.has(turnId); + const key = retainedOpenAiTurnKey(turnId); + return this.#settlingTurnIds.has(key) || this.#terminalTurns.has(key); } markTurnStarted(turnId: string): boolean { - if (this.#startedTurnIds.has(turnId) || this.hasTerminal(turnId)) { + if (this.#startedTurnIds.has(retainedOpenAiTurnKey(turnId)) || this.hasTerminal(turnId)) { return false; } @@ -65,13 +77,17 @@ export class OpenAiTurnTracker { return true; } + hasTurnStarted(turnId: string): boolean { + return this.#startedTurnIds.has(retainedOpenAiTurnKey(turnId)); + } + rejectTurn(turnId: string, error: Error): boolean { return this.settle(turnId, { error, kind: "failed" }); } rejectActiveTurns(error: Error): void { for (const turnId of this.activeTurnIds()) { - this.#settlingTurnIds.delete(turnId); + this.#settlingTurnIds.delete(retainedOpenAiTurnKey(turnId)); this.rejectTurn(turnId, error); } } @@ -81,16 +97,17 @@ export class OpenAiTurnTracker { return false; } - this.#settlingTurnIds.add(turnId); + this.#settlingTurnIds.add(retainedOpenAiTurnKey(turnId)); return true; } cancelSettlement(turnId: string): void { - this.#settlingTurnIds.delete(turnId); + this.#settlingTurnIds.delete(retainedOpenAiTurnKey(turnId)); } finishSettlement(turnId: string, terminalTurn: TerminalOpenAiTurn): boolean { - if (!this.#settlingTurnIds.delete(turnId) || this.#terminalTurns.has(turnId)) { + const key = retainedOpenAiTurnKey(turnId); + if (!this.#settlingTurnIds.delete(key) || this.#terminalTurns.has(key)) { return false; } @@ -108,9 +125,10 @@ export class OpenAiTurnTracker { } #recordTerminal(turnId: string, terminalTurn: TerminalOpenAiTurn): void { - this.#startedTurnIds.delete(turnId); + const key = retainedOpenAiTurnKey(turnId); + this.#startedTurnIds.delete(key); rememberBounded(this.#terminalTurns, turnId, terminalTurn); - const activeTurn = this.#activeTurns.get(turnId); + const activeTurn = this.#activeTurns.get(key); if (activeTurn === undefined) { return; @@ -122,11 +140,12 @@ export class OpenAiTurnTracker { activeTurn.reject(terminalTurn.error); } - this.#activeTurns.delete(turnId); + this.#activeTurns.delete(key); } async track(turnId: string, runId: RunId): Promise { - const terminalTurn = this.#terminalTurns.get(turnId); + const key = retainedOpenAiTurnKey(turnId); + const terminalTurn = this.#terminalTurns.get(key); if (terminalTurn?.kind === "completed") { return; @@ -136,18 +155,19 @@ export class OpenAiTurnTracker { throw terminalTurn.error; } - const activeTurn = this.#activeTurns.get(turnId); + const activeTurn = this.#activeTurns.get(key); if (activeTurn !== undefined) { if (activeTurn.runId !== runId) { - throw new Error(`Turn ${turnId} is already tracked by another run.`); + throw new Error("OpenAI turn is already tracked by another run."); } return activeTurn.promise; } const turn = Promise.withResolvers(); - this.#activeTurns.set(turnId, { + this.#activeTurns.set(key, { + nativeTurnId: turnId, promise: turn.promise, reject: turn.reject, resolve: () => { diff --git a/src/runtimes/openai/app-server-turn-validation.ts b/src/runtimes/openai/app-server-turn-validation.ts new file mode 100644 index 0000000..2ad6a73 --- /dev/null +++ b/src/runtimes/openai/app-server-turn-validation.ts @@ -0,0 +1,19 @@ +import { isRecord } from "./app-server-json"; +import type { JsonObject } from "./app-server-json"; + +export function validateTurnStatusError(turn: JsonObject, requireTerminal: boolean): string | null { + const status = turn["status"]; + + if ( + requireTerminal && + status !== "completed" && + status !== "failed" && + status !== "interrupted" + ) { + return "turn.status must be terminal."; + } + + return (status === "failed") !== isRecord(turn["error"]) + ? "turn.error must be present exactly when the turn failed." + : null; +} diff --git a/src/runtimes/openai/contract-adapter-state.ts b/src/runtimes/openai/contract-adapter-state.ts deleted file mode 100644 index 9533029..0000000 --- a/src/runtimes/openai/contract-adapter-state.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { AuthorityOutcomeUnknownError } from "../../contract"; -import type { JsonRpcId } from "./app-server-json"; -import type { PendingServerRequest } from "./contract-interactions"; -import { OpenAiPrivateCitationStreamFilter } from "./private-citation-filter"; -import type { ContractProjection } from "../contract-projection"; - -export class OpenAiContractAdapterState { - readonly #interactions = new Map(); - readonly #maxPendingServerRequestBytes: number; - readonly #messageFilters = new Map(); - #pendingServerRequestBytes = 0; - readonly #receiptTimes = new Map(); - readonly #textEncoder = new TextEncoder(); - #unknownReceiptEventId: string | undefined; - - constructor(maxPendingServerRequestBytes: number) { - this.#maxPendingServerRequestBytes = maxPendingServerRequestBytes; - } - - findInteraction(requestId: JsonRpcId): PendingServerRequest | undefined { - return [...this.#interactions.values()].find((pending) => pending.requestId === requestId); - } - - interaction(interactionId: string): PendingServerRequest | undefined { - return this.#interactions.get(interactionId); - } - - reserveInteraction(pending: Omit): PendingServerRequest { - const bytes = this.#textEncoder.encode(JSON.stringify(pending)).byteLength; - - if (bytes > this.#maxPendingServerRequestBytes - this.#pendingServerRequestBytes) { - throw new RangeError("OpenAI app-server pending request budget is exhausted."); - } - - const tracked = { ...pending, bytes }; - this.#interactions.set(pending.interaction.id, tracked); - this.#pendingServerRequestBytes += bytes; - return tracked; - } - - async commitInteraction( - pending: PendingServerRequest, - projection: ContractProjection, - ): Promise { - if (projection.interaction(pending.interaction.id) !== undefined) { - return; - } - - pending.commit ??= projection - .putInteraction( - pending.interaction.runId, - pending.method, - { - providerEventId: `${pending.method}:${String(pending.requestId)}`.slice(0, 256), - type: "provider", - }, - pending.interaction, - ) - .then(() => {}) - .finally(() => { - pending.commit = undefined; - }); - await pending.commit; - } - - dropInteraction(interactionId: string): PendingServerRequest | undefined { - const pending = this.#interactions.get(interactionId); - - if (pending !== undefined) { - this.#pendingServerRequestBytes -= pending.bytes; - this.#interactions.delete(interactionId); - } - - return pending; - } - - releaseTurn(turnId: string): PendingServerRequest[] { - const dropped: PendingServerRequest[] = []; - - for (const [id, pending] of this.#interactions) { - if (pending.turnId === turnId) { - this.dropInteraction(id); - dropped.push(pending); - } - } - - const prefix = `${turnId}\u0000`; - for (const key of this.#messageFilters.keys()) { - if (key.startsWith(prefix)) { - this.#messageFilters.delete(key); - } - } - - return dropped; - } - - messageFilter(turnId: string, itemId: string): OpenAiPrivateCitationStreamFilter { - const key = `${turnId}\u0000${itemId}`; - let filter = this.#messageFilters.get(key); - - if (filter === undefined) { - filter = new OpenAiPrivateCitationStreamFilter(); - this.#messageFilters.set(key, filter); - } - - return filter; - } - - deleteMessageFilter(turnId: string, itemId: string): void { - this.#messageFilters.delete(`${turnId}\u0000${itemId}`); - } - - async withReceiptTime( - eventId: string, - now: () => string, - operation: (occurredAt: string) => Promise, - ): Promise { - const occurredAt = this.#receiptTimes.get(eventId) ?? now(); - this.#receiptTimes.set(eventId, occurredAt); - - try { - const result = await operation(occurredAt); - this.#receiptTimes.delete(eventId); - if (this.#unknownReceiptEventId === eventId) { - this.#unknownReceiptEventId = undefined; - } - return result; - } catch (error) { - if (error instanceof AuthorityOutcomeUnknownError) { - this.#unknownReceiptEventId ??= eventId; - - if (this.#unknownReceiptEventId !== eventId) { - this.#receiptTimes.delete(eventId); - } - } else { - this.#receiptTimes.delete(eventId); - if (this.#unknownReceiptEventId === eventId) { - this.#unknownReceiptEventId = undefined; - } - } - throw error; - } - } - - clear(): void { - this.#interactions.clear(); - this.#messageFilters.clear(); - this.#pendingServerRequestBytes = 0; - this.#receiptTimes.clear(); - this.#unknownReceiptEventId = undefined; - } -} diff --git a/src/runtimes/openai/contract-adapter-types.ts b/src/runtimes/openai/contract-adapter-types.ts deleted file mode 100644 index 235cce8..0000000 --- a/src/runtimes/openai/contract-adapter-types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { MutationCause, Run, TokenUsage } from "../../contract"; -import type { ContractAuthorityUpdate, ContractProjectionOptions } from "../contract-projection"; -import type { JsonRpcId } from "./app-server-json"; - -export interface OpenAiTurnState { - cause: MutationCause; - run: Run; - runId: string; - threadId: string; - turnId: string; - usageBaseline?: TokenUsage; -} - -export interface PendingOpenAiTurnAttachment { - cause: MutationCause; - mutationId: string; - run: Run; - task?: Promise | undefined; - turn: OpenAiTurnState; -} - -export interface OpenAiAuthorityUpdate extends ContractAuthorityUpdate { - readonly turnId: string; -} - -export interface OpenAiContractAdapterOptions extends Omit { - readonly authority: (update: OpenAiAuthorityUpdate) => Promise; - readonly createId?: (() => string) | undefined; - readonly interactionTimeoutMs?: number | undefined; - readonly maxPendingServerRequestBytes?: number | undefined; -} - -export interface OpenAiTurnAttachment { - readonly cause: MutationCause; - readonly run: Run; - readonly threadId: string; - readonly turnId: string; -} - -export interface OpenAiServerReply { - readonly id: JsonRpcId; - readonly result: unknown; -} diff --git a/src/runtimes/openai/contract-adapter.ts b/src/runtimes/openai/contract-adapter.ts deleted file mode 100644 index 7a18749..0000000 --- a/src/runtimes/openai/contract-adapter.ts +++ /dev/null @@ -1,743 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import { - assertProtocolAdmission, - authorityContent, - interactionSchema, - runSchema, -} from "../../contract"; -import type { - Interaction, - InteractionResolution, - Item, - ProtocolAdmissionLimits, -} from "../../contract"; -import { createDriverId } from "../../protocol/id"; -import { readArray, readNonEmptyString, readRecord, readString } from "./app-server-json"; -import type { JsonObject, JsonRpcId } from "./app-server-json"; -import { ContractProjection } from "../contract-projection"; -import { - monotonicUsage, - type NativeItemLifecycle, - projectOpenAiItem, - providerEventId, - provenance, - readFiniteNumber, - requireRecord, - requireString, - subtractUsage, - toUsage, -} from "./contract-items"; -import { - type PendingServerRequest, - projectOpenAiInteraction, - toOpenAiRequestResult, -} from "./contract-interactions"; -import { OpenAiContractTurnInbox } from "./contract-turn-inbox"; -import { finishOpenAiTurn, projectOpenAiPlan } from "./contract-turn-lifecycle"; -import { OpenAiContractAdapterState } from "./contract-adapter-state"; -import type { - OpenAiAuthorityUpdate, - OpenAiContractAdapterOptions, - OpenAiServerReply, - OpenAiTurnAttachment, - OpenAiTurnState, - PendingOpenAiTurnAttachment, -} from "./contract-adapter-types"; - -export { OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION } from "./contract-interactions"; - -const DEFAULT_INTERACTION_TIMEOUT_MS = 5 * 60 * 1_000; -const DEFAULT_PENDING_SERVER_REQUEST_BYTES = 8 * 1_024 * 1_024; -export type { - OpenAiAuthorityUpdate, - OpenAiContractAdapterOptions, - OpenAiServerReply, - OpenAiTurnAttachment, -} from "./contract-adapter-types"; - -export class OpenAiContractAdapter { - readonly #admissionLimits: ProtocolAdmissionLimits | undefined; - readonly #authority: OpenAiContractAdapterOptions["authority"]; - readonly #createId: () => string; - readonly #inbox: OpenAiContractTurnInbox; - readonly #interactionTimeoutMs: number; - readonly #maxPendingServerRequestBytes: number; - readonly #pendingTurns = new Map(); - readonly #projection: ContractProjection; - readonly #sessionId: string; - readonly #state: OpenAiContractAdapterState; - readonly #turns = new Map(); - #disposed = false; - - constructor(options: OpenAiContractAdapterOptions) { - this.#admissionLimits = - options.admissionLimits === undefined ? undefined : { ...options.admissionLimits }; - this.#authority = options.authority; - this.#createId = options.createId ?? createDriverId; - this.#interactionTimeoutMs = options.interactionTimeoutMs ?? DEFAULT_INTERACTION_TIMEOUT_MS; - this.#maxPendingServerRequestBytes = - options.maxPendingServerRequestBytes ?? DEFAULT_PENDING_SERVER_REQUEST_BYTES; - this.#state = new OpenAiContractAdapterState(this.#maxPendingServerRequestBytes); - this.#sessionId = options.sessionId; - this.#projection = new ContractProjection({ - admissionLimits: this.#admissionLimits, - authority: async (update) => { - const turn = [...this.#turns.values()].find((value) => value.runId === update.runId); - - if (turn === undefined) { - throw new Error(`OpenAI app-server update references unknown run ${update.runId}.`); - } - - await options.authority({ ...update, turnId: turn.turnId }); - }, - now: options.now, - preview: options.preview, - previewCheckpointBytes: options.previewCheckpointBytes, - previewReplaceIntervalMs: options.previewReplaceIntervalMs, - sessionId: options.sessionId, - }); - this.#inbox = new OpenAiContractTurnInbox({ - dispatch: (method, params) => this.#dispatchNotification(method, params), - replayEnd: (params) => this.#onTurnEnd(params, "turn/completed"), - }); - - if ( - [this.#interactionTimeoutMs, this.#maxPendingServerRequestBytes].some( - (value) => !Number.isSafeInteger(value) || value < 1, - ) - ) { - throw new RangeError("OpenAI Contract adapter limits must be finite and positive."); - } - } - - /** Bind turn/started to the Run already created by the Coordinator. */ - async attachTurn(input: OpenAiTurnAttachment): Promise { - this.#assertActive(); - const run = runSchema.parse({ - ...input.run, - provenance: provenance("turn/started", { - threadId: input.threadId, - turnId: input.turnId, - }), - }); - - if (run.status !== "active") { - throw new Error(`OpenAI turn ${input.turnId} requires an active run.`); - } - - const existing = this.#turns.get(input.turnId); - - if (existing !== undefined) { - if (existing.runId !== run.id || existing.threadId !== input.threadId) { - throw new Error(`OpenAI turn ${input.turnId} is already registered to another run.`); - } - if ( - !isDeepStrictEqual(existing.run, run) || - !isDeepStrictEqual(existing.cause, input.cause) - ) { - throw new Error(`OpenAI turn ${input.turnId} is registered with different state.`); - } - - this.#projection.attachRun(run); - await this.#inbox.replay(input.turnId); - return; - } - - const ended = this.#inbox.ended(input.turnId); - - if (ended !== undefined) { - if (ended.runId !== run.id || ended.threadId !== input.threadId) { - throw new Error(`OpenAI turn ${input.turnId} is already registered to another run.`); - } - if (!isDeepStrictEqual(ended.run, run) || !isDeepStrictEqual(ended.cause, input.cause)) { - throw new Error(`OpenAI turn ${input.turnId} is registered with different state.`); - } - return; - } - - const pending = this.#pendingTurns.get(input.turnId); - - if (pending !== undefined) { - if ( - pending.turn.runId !== run.id || - pending.turn.threadId !== input.threadId || - !isDeepStrictEqual(pending.run, run) || - !isDeepStrictEqual(pending.cause, input.cause) - ) { - throw new Error(`OpenAI turn ${input.turnId} changed while its attachment was pending.`); - } - - await this.#commitTurn(pending); - await this.#inbox.replay(input.turnId); - return; - } - - if ( - [ - ...this.#turns.values(), - ...[...this.#pendingTurns.values()].map((entry) => entry.turn), - ].some((turn) => turn.runId === run.id) - ) { - throw new Error(`OpenAI run ${run.id} is already attached to another turn.`); - } - - const turn = { - cause: input.cause, - run, - runId: run.id, - threadId: input.threadId, - turnId: input.turnId, - }; - const attachment = { - cause: input.cause, - mutationId: this.#createId(), - run, - turn, - }; - this.#pendingTurns.set(input.turnId, attachment); - await this.#commitTurn(attachment); - await this.#inbox.replay(input.turnId); - } - - async handleNotification(method: string, value: unknown): Promise { - this.#assertActive(); - const params = requireRecord(value, `${method} params`); - const eventTurnId = - readNonEmptyString(params, "turnId") ?? readNonEmptyString(readRecord(params, "turn"), "id"); - - if (eventTurnId !== null && this.#inbox.hasEnded(eventTurnId)) { - return; - } - - if ( - eventTurnId !== null && - (!this.#turns.has(eventTurnId) || this.#inbox.shouldBuffer(eventTurnId)) - ) { - if (method === "turn/completed") { - this.#inbox.rememberEnd(eventTurnId, params); - } else { - this.#inbox.rememberNotification(eventTurnId, method, params); - } - return; - } - - await this.#dispatchNotification(method, params); - } - - async #dispatchNotification(method: string, params: JsonObject): Promise { - switch (method) { - case "item/started": - await this.#onItem(params, "started", method); - return; - case "item/completed": - await this.#onItem(params, "completed", method); - return; - case "item/agentMessage/delta": - await this.#onText(params, method, "message.text", "delta"); - return; - case "item/reasoning/summaryPartAdded": - await this.#onReasoningPart(params, method); - return; - case "item/reasoning/summaryTextDelta": - await this.#onText(params, method, "reasoning.text", "delta"); - return; - case "item/commandExecution/outputDelta": - await this.#onText(params, method, "terminal.stdout", "delta"); - return; - case "item/mcpToolCall/progress": - await this.#onProgress(params, method); - return; - case "serverRequest/resolved": - await this.#onRequestResolved(params, method); - return; - case "item/fileChange/patchUpdated": - await this.#onPatch(params, method); - return; - case "turn/plan/updated": - await this.#onPlan(params, method); - return; - case "thread/tokenUsage/updated": - await this.#onUsage(params, method); - return; - case "turn/completed": - await this.#onTurnEnd(params, method); - return; - default: - return; - } - } - - async handleServerRequest( - method: string, - requestId: JsonRpcId, - value: unknown, - ): Promise { - this.#assertActive(); - const params = requireRecord(value, `${method} params`); - const turnId = readNonEmptyString(params, "turnId"); - - if (turnId === null) { - return null; - } - - const turn = this.#turns.get(turnId); - - if (turn === undefined) { - throw new Error(`OpenAI app-server request ${String(requestId)} arrived before attachment.`); - } - - if (this.#projection.run(turn.runId)?.status !== "active") { - return null; - } - - const existing = this.#state.findInteraction(requestId); - - if (existing !== undefined) { - const pending = existing; - - if ( - pending.method !== method || - pending.turnId !== turnId || - !isDeepStrictEqual(pending.params, params) - ) { - throw new Error( - `OpenAI app-server request ${String(requestId)} changed identity or content.`, - ); - } - - await this.#state.commitInteraction(pending, this.#projection); - return pending.interaction.id; - } - - const interaction = this.#projectInteraction(method, requestId, params, turn); - - if (interaction === null) { - return null; - } - - const pending = { - interaction, - method, - params: structuredClone(params), - requestId, - turnId, - }; - const tracked = this.#state.reserveInteraction(pending); - await this.#state.commitInteraction(tracked, this.#projection); - return interaction.id; - } - - resolveInteraction( - interactionId: string, - resolution: InteractionResolution, - ): OpenAiServerReply | null { - this.#assertActive(); - const pending = this.#state.interaction(interactionId); - - if (pending === undefined) { - return null; - } - - const result = this.#toRequestResult(pending, resolution); - this.#dropInteraction(interactionId); - return { id: pending.requestId, result }; - } - - dispose(): void { - this.#disposed = true; - this.#projection.dispose(); - this.#inbox.dispose(); - this.#pendingTurns.clear(); - this.#state.clear(); - this.#turns.clear(); - } - - async #onItem(params: JsonObject, lifecycle: NativeItemLifecycle, method: string): Promise { - const turn = this.#requireTurn(params, method); - const item = requireRecord(params["item"], `${method} params.item`); - const itemId = requireString(item, "id", `${method} params.item`); - const existing = this.#projection.item(turn.runId, itemId); - - if (existing !== undefined && existing.status !== "active") { - return; - } - - const eventId = providerEventId(method, params); - await this.#withReceiptTime(eventId, async (occurredAt) => { - const projected = this.#projectItem(turn, item, lifecycle, occurredAt, method); - - if (projected === null) { - return; - } - - await this.#projection.putItem( - turn.runId, - method, - { providerEventId: eventId, type: "provider" }, - projected, - ); - }); - - if (lifecycle === "completed") { - this.#state.deleteMessageFilter(turn.turnId, itemId); - } - } - - async #onPatch(params: JsonObject, method: string): Promise { - const turn = this.#requireTurn(params, method); - const itemId = requireString(params, "itemId", `${method} params`); - const existing = this.#projection.item(turn.runId, itemId); - - if (existing !== undefined && existing.status !== "active") { - return; - } - - const eventId = providerEventId(method, params); - await this.#withReceiptTime(eventId, async (occurredAt) => { - const projected = this.#projectItem( - turn, - { - changes: readArray(params, "changes"), - id: itemId, - status: "inProgress", - type: "fileChange", - }, - "started", - occurredAt, - method, - ); - - if (projected === null) { - return; - } - - await this.#projection.putItem( - turn.runId, - method, - { providerEventId: eventId, type: "provider" }, - projected, - ); - }); - } - - async #onPlan(params: JsonObject, method: string): Promise { - const turn = this.#requireTurn(params, method); - const itemId = "turn-plan"; - const previous = this.#projection.item(turn.runId, itemId); - const eventId = providerEventId(method, params); - await this.#withReceiptTime(eventId, async (occurredAt) => { - const plan = projectOpenAiPlan(turn, params, occurredAt, method, previous); - - await this.#projection.putItem( - turn.runId, - method, - { providerEventId: eventId, type: "provider" }, - plan, - ); - }); - } - - async #onUsage(params: JsonObject, method: string): Promise { - const turn = this.#requireTurn(params, method); - const tokenUsage = readRecord(params, "tokenUsage"); - const last = toUsage(readRecord(tokenUsage, "last")); - const total = toUsage(readRecord(tokenUsage, "total")); - - if (last === undefined && total === undefined) { - return; - } - - if (total !== undefined && turn.usageBaseline === undefined) { - turn.usageBaseline = subtractUsage(total, last ?? total); - } - - const candidate = - total === undefined ? last! : subtractUsage(total, turn.usageBaseline ?? total); - const usage = monotonicUsage(this.#projection.run(turn.runId)?.usage, candidate); - - await this.#projection.updateUsage( - turn.runId, - method, - { providerEventId: providerEventId(method, params), type: "provider" }, - usage, - ); - } - - async #onTurnEnd(params: JsonObject, method: string): Promise { - const nativeTurn = requireRecord(params["turn"], `${method} params.turn`); - const turnId = requireString(nativeTurn, "id", `${method} params.turn`); - const turn = this.#turns.get(turnId); - - if (turn === undefined || this.#projection.run(turn.runId)?.status !== "active") { - return; - } - - await finishOpenAiTurn({ - method, - params, - projectItem: (state, item, lifecycle, occurredAt, event) => - this.#projectItem(state, item, lifecycle, occurredAt, event), - projection: this.#projection, - release: (id) => this.#releaseTurn(id), - rememberEnded: (state) => this.#inbox.rememberEnded(state.turnId, state), - turn, - withReceiptTime: (eventId, operation) => this.#withReceiptTime(eventId, operation), - }); - } - - async #onRequestResolved(params: JsonObject, method: string): Promise { - const requestId = params["requestId"]; - const threadId = requireString(params, "threadId", `${method} params`); - - if (typeof requestId !== "number" && typeof requestId !== "string") { - throw new Error(`${method} params.requestId must be a string or number.`); - } - - const pending = this.#state.findInteraction(requestId); - - if (pending === undefined) { - return; - } - - const interactionId = pending.interaction.id; - const turn = this.#turns.get(pending.turnId); - - if (turn === undefined) { - this.#dropInteraction(interactionId); - return; - } - - if (turn.threadId !== threadId) { - throw new Error(`${method} params references the wrong thread.`); - } - - const eventId = `${method}:${String(requestId)}`.slice(0, 256); - await this.#withReceiptTime(eventId, async (endedAt) => { - const interaction = interactionSchema.parse({ - ...pending.interaction, - endedAt, - status: "expired", - }); - - await this.#projection.putInteraction( - turn.runId, - method, - { providerEventId: eventId, type: "provider" }, - interaction, - ); - this.#dropInteraction(interactionId); - }); - } - - async #onText( - params: JsonObject, - method: string, - channel: "message.text" | "reasoning.text" | "terminal.stdout", - textField: string, - ): Promise { - const turn = this.#requireTurn(params, method); - const itemId = requireString(params, "itemId", `${method} params`); - const rawDelta = readString(params, textField); - const item = this.#projection.item(turn.runId, itemId); - - if ( - rawDelta === null || - rawDelta.length === 0 || - item === undefined || - item.status !== "active" || - (channel === "message.text" && item.kind !== "message") || - (channel === "reasoning.text" && item.kind !== "reasoning") || - (channel === "terminal.stdout" && item.kind !== "terminal") - ) { - return; - } - - const delta = - channel === "message.text" - ? this.#messageFilter(turn.turnId, itemId).push(rawDelta).text - : rawDelta; - - if (delta.length === 0) { - return; - } - - await this.#projection.appendText({ - cause: { - providerEventId: `${providerEventId(method, params)}:checkpoint`.slice(0, 256), - type: "provider", - }, - channel, - delta, - event: method, - itemId, - runId: turn.runId, - }); - } - - async #onReasoningPart(params: JsonObject, method: string): Promise { - const summaryIndex = readFiniteNumber(params, "summaryIndex"); - - if (summaryIndex === null || !Number.isInteger(summaryIndex) || summaryIndex < 1) { - return; - } - - const turn = this.#requireTurn(params, method); - const itemId = requireString(params, "itemId", `${method} params`); - const item = this.#projection.item(turn.runId, itemId); - - if (item === undefined || item.status !== "active" || item.kind !== "reasoning") { - return; - } - - await this.#projection.appendText({ - cause: { - providerEventId: `${providerEventId(method, params)}:checkpoint`.slice(0, 256), - type: "provider", - }, - channel: "reasoning.text", - delta: "\n\n", - event: method, - itemId, - runId: turn.runId, - }); - } - - async #onProgress(params: JsonObject, method: string): Promise { - const turn = this.#requireTurn(params, method); - const itemId = requireString(params, "itemId", `${method} params`); - const message = readString(params, "message"); - - if (message === null) { - return; - } - - await this.#projection.replacePreview({ - channel: "tool.progress", - itemId, - runId: turn.runId, - text: message, - }); - } - - #projectItem( - turn: OpenAiTurnState, - native: JsonObject, - lifecycle: NativeItemLifecycle, - occurredAt: string, - event: string, - ): Item | null { - return projectOpenAiItem(turn, native, lifecycle, occurredAt, event, (runId, itemId) => - this.#projection.item(runId, itemId), - ); - } - - #projectInteraction( - method: string, - requestId: JsonRpcId, - params: JsonObject, - turn: OpenAiTurnState, - ): Interaction | null { - return projectOpenAiInteraction(method, requestId, params, turn, { - createId: this.#createId, - interactionTimeoutMs: this.#interactionTimeoutMs, - item: (runId, itemId) => this.#projection.item(runId, itemId), - now: () => this.#projection.now(), - }); - } - - #toRequestResult(pending: PendingServerRequest, resolution: InteractionResolution): unknown { - return toOpenAiRequestResult(pending, resolution); - } - - #requireTurn(params: JsonObject, method: string): OpenAiTurnState { - const turnId = requireString(params, "turnId", `${method} params`); - const turn = this.#turns.get(turnId); - - if (turn === undefined) { - throw new Error(`OpenAI app-server event ${method} references unknown turn ${turnId}.`); - } - - const threadId = readNonEmptyString(params, "threadId"); - - if (threadId !== null && threadId !== turn.threadId) { - throw new Error(`OpenAI app-server event ${method} references the wrong thread.`); - } - - return turn; - } - - async #commitTurn(pending: PendingOpenAiTurnAttachment): Promise { - const update: OpenAiAuthorityUpdate = { - cause: pending.cause, - event: "turn/started", - mutationId: pending.mutationId, - operations: [{ entity: "run", op: "put", value: pending.run }], - runId: pending.run.id, - sessionId: this.#sessionId, - turnId: pending.turn.turnId, - }; - pending.task ??= Promise.resolve() - .then(async () => { - this.#assertActive(); - if (this.#admissionLimits !== undefined) { - assertProtocolAdmission( - update, - this.#admissionLimits, - authorityContent(update.operations), - ); - } - - await this.#authority(update); - - if (this.#disposed) { - return; - } - - this.#turns.set(pending.turn.turnId, pending.turn); - this.#projection.attachRun(pending.run); - this.#pendingTurns.delete(pending.turn.turnId); - }) - .finally(() => { - pending.task = undefined; - }); - await pending.task; - } - - async #withReceiptTime( - eventId: string, - operation: (occurredAt: string) => Promise, - ): Promise { - return this.#state.withReceiptTime( - eventId, - () => this.#projection.now().toISOString(), - operation, - ); - } - - #dropInteraction(interactionId: string): void { - const pending = this.#state.dropInteraction(interactionId); - - if (pending !== undefined) { - this.#projection.releaseInteraction(interactionId); - } - } - - #releaseTurn(turnId: string): void { - this.#turns.delete(turnId); - - for (const pending of this.#state.releaseTurn(turnId)) { - this.#projection.releaseInteraction(pending.interaction.id); - } - } - - #messageFilter(turnId: string, itemId: string) { - return this.#state.messageFilter(turnId, itemId); - } - - #assertActive(): void { - if (this.#disposed) { - throw new Error("OpenAI Contract adapter is disposed."); - } - } -} diff --git a/src/runtimes/openai/contract-interactions.ts b/src/runtimes/openai/contract-interactions.ts deleted file mode 100644 index 2523856..0000000 --- a/src/runtimes/openai/contract-interactions.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { interactionSchema } from "../../contract"; -import type { Interaction, InteractionResolution, Item } from "../../contract"; -import { asJsonValue } from "../contract-adapter-meta"; -import { isRecord, readArray, readNonEmptyString, readString } from "./app-server-json"; -import type { JsonObject, JsonRpcId } from "./app-server-json"; -import { - dynamicToolName, - provenance, - readFiniteNumber, - requireRecord, - toNativeToolContent, -} from "./contract-items"; - -export const OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION = "openai.app-server/mcp-elicitation"; - -export interface OpenAiInteractionTurn { - readonly runId: string; - readonly threadId: string; - readonly turnId: string; -} - -export interface PendingServerRequest { - bytes: number; - commit?: Promise | undefined; - interaction: Interaction; - method: string; - params: JsonObject; - requestId: JsonRpcId; - turnId: string; -} - -export function toInputAnswer(params: JsonObject, questionId: string, answer: string): string { - const question = readArray(params, "questions").find( - (entry) => isRecord(entry) && readString(entry, "id") === questionId, - ); - - if (!isRecord(question)) { - return answer; - } - - for (const [index, option] of readArray(question, "options").entries()) { - if (!isRecord(option)) { - continue; - } - - const label = readNonEmptyString(option, "label"); - - if (label !== null && String(index) === answer) { - return label; - } - } - - return answer; -} - -export function selectedOption( - interaction: Interaction, - resolution: Extract["value"], -): string | null { - if (resolution.type === "cancelled") { - return null; - } - - if ( - interaction.kind !== "permission" || - !interaction.request.options.some((option) => option.id === resolution.optionId) - ) { - throw new Error("OpenAI permission resolution selected an unavailable option."); - } - - return resolution.optionId; -} - -export function projectOpenAiInteraction( - method: string, - requestId: JsonRpcId, - params: JsonObject, - turn: OpenAiInteractionTurn, - options: { - readonly createId: () => string; - readonly interactionTimeoutMs: number; - readonly item: (runId: string, itemId: string) => Item | undefined; - readonly now: () => Date; - }, -): Interaction | null { - const createdAt = options.now().toISOString(); - const itemId = - readNonEmptyString(params, "itemId") ?? - (method === "item/tool/call" ? readNonEmptyString(params, "callId") : null); - const knownItem = - itemId !== null && options.item(turn.runId, itemId) !== undefined ? itemId : undefined; - const requestedTimeoutMs = readFiniteNumber(params, "autoResolutionMs"); - const timeoutMs = - requestedTimeoutMs !== null && - Number.isSafeInteger(requestedTimeoutMs) && - requestedTimeoutMs > 0 - ? Math.min(requestedTimeoutMs, options.interactionTimeoutMs) - : options.interactionTimeoutMs; - const common = { - audience: "participants", - blocking: true, - createdAt, - expiresAt: new Date(Date.parse(createdAt) + timeoutMs).toISOString(), - id: options.createId(), - ...(knownItem === undefined ? {} : { itemId: knownItem }), - provenance: provenance(method, { - ...(itemId === null ? {} : { itemId }), - requestId: String(requestId), - threadId: turn.threadId, - turnId: turn.turnId, - }), - runId: turn.runId, - status: "open", - }; - - if ( - method === "item/commandExecution/requestApproval" || - method === "item/fileChange/requestApproval" || - method === "item/permissions/requestApproval" - ) { - const command = readNonEmptyString(params, "command"); - const reason = readString(params, "reason"); - const availableDecisions = params["availableDecisions"]; - const allowed = - method === "item/commandExecution/requestApproval" && Array.isArray(availableDecisions) - ? new Set(availableDecisions.filter((value) => typeof value === "string")) - : null; - const options = [ - { - decision: "accept", - effect: "allow", - id: "accept_once", - label: "Allow once", - scope: "once", - }, - { - decision: "acceptForSession", - effect: "allow", - id: "accept_session", - label: "Allow for session", - scope: "session", - }, - { decision: "decline", effect: "deny", id: "decline", label: "Decline", scope: "once" }, - ].flatMap(({ decision, ...option }) => - allowed === null || allowed.has(decision) ? [option] : [], - ); - - if (options.length === 0) { - return null; - } - - return interactionSchema.parse({ - ...common, - kind: "permission", - request: { - ...(reason === null ? {} : { description: reason }), - options, - subject: - knownItem === undefined - ? { - operation: method, - targets: [itemId ?? command ?? method], - type: "resource", - } - : { itemId: knownItem, type: "item" }, - title: - method === "item/fileChange/requestApproval" - ? "Approve file changes" - : method === "item/permissions/requestApproval" - ? "Approve runtime permissions" - : "Approve command execution", - }, - }); - } - - if (method === "item/tool/requestUserInput") { - const questions = readArray(params, "questions").flatMap((entry) => { - if (!isRecord(entry)) { - return []; - } - - const id = readNonEmptyString(entry, "id"); - const prompt = readNonEmptyString(entry, "question"); - - if (id === null || prompt === null) { - return []; - } - - const nativeOptions = entry["options"]; - const mappedOptions = Array.isArray(nativeOptions) - ? nativeOptions.flatMap((option, index) => { - if (!isRecord(option)) { - return []; - } - - const label = readNonEmptyString(option, "label"); - const description = readString(option, "description"); - return label === null - ? [] - : [ - { - ...(description === null ? {} : { description }), - id: String(index), - label, - }, - ]; - }) - : []; - const options = mappedOptions.length === 0 ? undefined : mappedOptions; - - return [ - { - ...(options !== undefined && entry["isOther"] === true ? { allowOther: true } : {}), - id, - ...(options === undefined ? {} : { options }), - prompt, - required: true, - type: - options === undefined - ? entry["isSecret"] === true - ? "secret" - : "text" - : "single_select", - }, - ]; - }); - - return questions.length === 0 - ? null - : interactionSchema.parse({ - ...common, - kind: "input", - request: { questions }, - }); - } - - if (method === "item/tool/call") { - const tool = dynamicToolName(params); - const input = asJsonValue(params["arguments"]); - - return tool === null - ? null - : interactionSchema.parse({ - ...common, - kind: "tool", - request: { - ...(input === undefined ? {} : { input }), - name: tool, - }, - }); - } - - if (method === "mcpServer/elicitation/request") { - const request = asJsonValue(params); - - return request === undefined - ? null - : interactionSchema.parse({ - ...common, - kind: "extension", - name: OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION, - request, - }); - } - - return null; -} - -export function toOpenAiRequestResult( - pending: PendingServerRequest, - resolution: InteractionResolution, -): unknown { - if ( - pending.method === "item/commandExecution/requestApproval" || - pending.method === "item/fileChange/requestApproval" - ) { - if (resolution.kind !== "permission") { - throw new Error("OpenAI approval request requires a permission resolution."); - } - - const optionId = selectedOption(pending.interaction, resolution.value); - const decision = - optionId === null - ? "cancel" - : optionId === "accept_once" - ? "accept" - : optionId === "accept_session" - ? "acceptForSession" - : optionId === "decline" - ? "decline" - : null; - - if (decision === null) { - throw new Error("OpenAI approval resolution selected an unknown option."); - } - - return { decision }; - } - - if (pending.method === "item/permissions/requestApproval") { - if (resolution.kind !== "permission") { - throw new Error("OpenAI permission profile request requires a permission resolution."); - } - - const optionId = selectedOption(pending.interaction, resolution.value); - const accepted = optionId === "accept_once" || optionId === "accept_session"; - - if ( - optionId !== null && - optionId !== "accept_once" && - optionId !== "accept_session" && - optionId !== "decline" - ) { - throw new Error("OpenAI permission resolution selected an unknown option."); - } - - return { - permissions: - accepted && isRecord(pending.params["permissions"]) ? pending.params["permissions"] : {}, - scope: optionId === "accept_session" ? "session" : "turn", - }; - } - - if (pending.method === "item/tool/requestUserInput") { - if (resolution.kind !== "input") { - throw new Error("OpenAI user input request requires an input resolution."); - } - - return { - answers: - resolution.value.type === "cancelled" - ? {} - : Object.fromEntries( - Object.entries(resolution.value.answers).map(([id, answers]) => [ - id, - { - answers: answers.map((answer) => toInputAnswer(pending.params, id, answer)), - }, - ]), - ), - }; - } - - if (pending.method === "item/tool/call") { - if (resolution.kind !== "tool") { - throw new Error("OpenAI dynamic tool request requires a tool resolution."); - } - - if (resolution.value.type === "completed") { - const contentItems = resolution.value.output.flatMap(toNativeToolContent); - - if (resolution.value.structuredOutput !== undefined) { - contentItems.push({ - text: JSON.stringify(resolution.value.structuredOutput), - type: "inputText", - }); - } - - return { contentItems, success: true }; - } - - const message = - resolution.value.type === "failed" ? resolution.value.error.message : "Tool call cancelled."; - return { - contentItems: [{ text: message, type: "inputText" }], - success: false, - }; - } - - if (pending.method === "mcpServer/elicitation/request") { - if ( - resolution.kind !== "extension" || - resolution.name !== OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION - ) { - throw new Error("OpenAI MCP elicitation requires its namespaced extension resolution."); - } - - const value = requireRecord(resolution.value, "OpenAI MCP elicitation resolution"); - const action = readString(value, "action"); - - if (action !== "accept" && action !== "decline" && action !== "cancel") { - throw new Error("OpenAI MCP elicitation resolution has an unsupported action."); - } - - return { - _meta: asJsonValue(value["_meta"]) ?? null, - action, - content: asJsonValue(value["content"]) ?? null, - }; - } - - throw new Error(`Unsupported OpenAI app-server request: ${pending.method}.`); -} diff --git a/src/runtimes/openai/contract-items.ts b/src/runtimes/openai/contract-items.ts deleted file mode 100644 index d8a332f..0000000 --- a/src/runtimes/openai/contract-items.ts +++ /dev/null @@ -1,543 +0,0 @@ -import { itemSchema } from "../../contract"; -import type { ContentBlock, FileChange, Item, ProtocolError, TokenUsage } from "../../contract"; -import { asJsonValue, createProviderMeta } from "../contract-adapter-meta"; -import { isRecord, readArray, readNonEmptyString, readRecord, readString } from "./app-server-json"; -import type { JsonObject } from "./app-server-json"; -import { filterOpenAiPrivateCitations } from "./private-citation-filter"; - -const PROVIDER_EXTENSION_ITEM = "openai.app-server/thread-item"; -const providerMeta = createProviderMeta("openai"); - -export type NativeItemLifecycle = "completed" | "started"; - -export interface OpenAiItemTurn { - readonly runId: string; - readonly threadId: string; - readonly turnId: string; -} - -type DynamicToolContentItem = - | { imageUrl: string; type: "inputImage" } - | { text: string; type: "inputText" }; - -export function requireRecord(value: unknown, label: string): JsonObject { - if (!isRecord(value)) { - throw new Error(`${label} must be an object.`); - } - - return value; -} - -export function requireString(value: JsonObject, key: string, label: string): string { - const entry = readNonEmptyString(value, key); - - if (entry === null) { - throw new Error(`${label}.${key} must be a non-empty string.`); - } - - return entry; -} - -export function readFiniteNumber(value: JsonObject, key: string): number | null { - const entry = value[key]; - return typeof entry === "number" && Number.isFinite(entry) ? entry : null; -} - -const usageKeys = ["cachedInput", "input", "output", "reasoning", "total"] as const; -const nativeUsageKeys = { - cachedInput: "cachedInputTokens", - input: "inputTokens", - output: "outputTokens", - reasoning: "reasoningOutputTokens", - total: "totalTokens", -} as const satisfies Record<(typeof usageKeys)[number], string>; - -export function toUsage(value: JsonObject | null): TokenUsage | undefined { - if (value === null) { - return undefined; - } - - const usage: TokenUsage = {}; - - for (const key of usageKeys) { - const entry = readFiniteNumber(value, nativeUsageKeys[key]); - - if (entry !== null && entry >= 0 && Number.isSafeInteger(entry)) { - usage[key] = entry; - } - } - - return Object.keys(usage).length > 0 ? usage : undefined; -} - -export function subtractUsage(total: TokenUsage, baseline: TokenUsage): TokenUsage { - const usage: TokenUsage = {}; - - for (const key of usageKeys) { - const value = total[key]; - - if (value !== undefined) { - usage[key] = Math.max(0, value - (baseline[key] ?? 0)); - } - } - - return usage; -} - -export function monotonicUsage(previous: TokenUsage | undefined, next: TokenUsage): TokenUsage { - const usage: TokenUsage = {}; - - for (const key of usageKeys) { - const value = next[key] ?? previous?.[key]; - - if (value !== undefined) { - usage[key] = Math.max(value, previous?.[key] ?? 0); - } - } - - return usage; -} - -export function latestTimestamp(previous: string | undefined, next: string): string { - return previous !== undefined && Date.parse(previous) > Date.parse(next) ? previous : next; -} - -export function textContent(text: string): ContentBlock[] { - return text.length === 0 ? [] : [{ text, type: "text" }]; -} - -export function providerEventId(method: string, params: JsonObject): string { - const ids = [ - readString(params, "turnId"), - readString(readRecord(params, "turn"), "id"), - readString(params, "itemId"), - readString(readRecord(params, "item"), "id"), - ].filter((entry) => entry !== null); - return [method, ...ids].join(":").slice(0, 256); -} - -export function provenance( - event: string, - input: { itemId?: string; requestId?: string; threadId: string; turnId: string }, -) { - return providerMeta.provenance(event, input); -} - -export function itemStatus(item: JsonObject, lifecycle: NativeItemLifecycle): Item["status"] { - if (lifecycle === "started") { - return "active"; - } - - const status = readString(item, "status"); - - if (status === "failed") { - return "failed"; - } - - if (status === "declined") { - return "cancelled"; - } - - return "completed"; -} - -export function itemError(item: JsonObject, type: string): ProtocolError { - const error = readRecord(item, "error"); - - return { - code: `openai.${type}.failed`, - message: readString(error, "message") ?? `${type} failed.`, - retryable: false, - }; -} - -export function toFileChanges(item: JsonObject): FileChange[] { - return readArray(item, "changes").flatMap((entry) => { - if (!isRecord(entry)) { - return []; - } - - const path = readNonEmptyString(entry, "path"); - const kind = readRecord(entry, "kind"); - const type = readString(kind, "type"); - - if (path === null || (type !== "add" && type !== "delete" && type !== "update")) { - return []; - } - - const movePath = readNonEmptyString(kind, "move_path"); - const diff = readString(entry, "diff"); - - if (type === "update" && movePath !== null) { - return [ - { - ...(diff === null || diff.length === 0 ? {} : { diff: { text: diff, type: "text" } }), - oldPath: path, - operation: "move", - path: movePath, - }, - ]; - } - - return [ - { - ...(diff === null || diff.length === 0 ? {} : { diff: { text: diff, type: "text" } }), - operation: type === "add" ? "create" : type === "delete" ? "delete" : "update", - path, - }, - ]; - }); -} - -export function dynamicToolName(value: JsonObject): string | null { - const tool = readNonEmptyString(value, "tool"); - const namespace = readNonEmptyString(value, "namespace"); - return tool === null ? null : namespace === null ? tool : `${namespace}/${tool}`; -} - -export function toNativeToolContent(block: ContentBlock): DynamicToolContentItem[] { - if (block.type === "text") { - return [{ text: block.text, type: "inputText" }]; - } - - if (block.type === "json") { - return [{ text: JSON.stringify(block.value), type: "inputText" }]; - } - - if (block.type === "resource_link" && block.mediaType?.startsWith("image/")) { - return [{ imageUrl: block.uri, type: "inputImage" }]; - } - - if (block.type === "inline_blob" && block.mediaType.startsWith("image/")) { - return [ - { - imageUrl: `data:${block.mediaType};base64,${block.data}`, - type: "inputImage", - }, - ]; - } - - if (block.type === "extension") { - return [{ text: JSON.stringify(block.value), type: "inputText" }]; - } - - return []; -} - -export function fromNativeToolContent(value: JsonObject | null): ContentBlock[] { - return readArray(value, "contentItems").flatMap((entry) => { - if (!isRecord(entry)) { - const json = asJsonValue(entry); - return json === undefined ? [] : [{ type: "json", value: json }]; - } - - if (readString(entry, "type") === "inputText") { - const text = readString(entry, "text"); - return text === null ? [] : [{ text, type: "text" }]; - } - - if (readString(entry, "type") === "inputImage") { - const imageUrl = readNonEmptyString(entry, "imageUrl"); - const dataUrl = imageUrl?.match(/^data:([^;,]+);base64,(.+)$/su); - - if (dataUrl?.[1] !== undefined && dataUrl[2] !== undefined) { - return [{ data: dataUrl[2], mediaType: dataUrl[1], type: "inline_blob" }]; - } - - if (imageUrl !== null && URL.canParse(imageUrl)) { - return [{ type: "resource_link", uri: imageUrl }]; - } - } - - const json = asJsonValue(entry); - return json === undefined ? [] : [{ type: "json", value: json }]; - }); -} - -export function projectOpenAiItem( - turn: OpenAiItemTurn, - native: JsonObject, - lifecycle: NativeItemLifecycle, - occurredAt: string, - event: string, - lookupItem: (runId: string, itemId: string) => Item | undefined, -): Item | null { - const id = readNonEmptyString(native, "id"); - const type = readString(native, "type"); - - if (id === null || type === null || type === "userMessage") { - return null; - } - - const existing = lookupItem(turn.runId, id); - const status = itemStatus(native, lifecycle); - const updatedAt = latestTimestamp(existing?.updatedAt, occurredAt); - const base = { - audience: type === "hookPrompt" ? "operators" : "participants", - createdAt: existing?.createdAt ?? occurredAt, - ...(status === "active" ? {} : { endedAt: updatedAt }), - ...(status === "failed" ? { error: itemError(native, type) } : {}), - id, - provenance: provenance(event, { - itemId: id, - threadId: turn.threadId, - turnId: turn.turnId, - }), - runId: turn.runId, - status, - updatedAt, - }; - if (type === "agentMessage") { - const nativeText = readString(native, "text"); - - if (nativeText === null) { - return null; - } - - const phase = readString(native, "phase"); - const text = filterOpenAiPrivateCitations(nativeText).text; - return itemSchema.parse({ - ...base, - content: textContent(text), - kind: "message", - ...(phase === "commentary" - ? { phase: "commentary" } - : phase === "final_answer" - ? { phase: "final" } - : {}), - role: "agent", - }); - } - - if (type === "reasoning") { - const text = readArray(native, "summary") - .filter((entry) => typeof entry === "string") - .join("\n\n"); - - return itemSchema.parse({ - ...base, - content: textContent(text), - kind: "reasoning", - }); - } - - if (type === "commandExecution") { - const command = readString(native, "command"); - - if (command === null) { - return null; - } - - const exitCode = readFiniteNumber(native, "exitCode"); - const aggregatedOutput = readString(native, "aggregatedOutput") ?? ""; - const cwd = readNonEmptyString(native, "cwd"); - - return itemSchema.parse({ - ...base, - command, - ...(cwd === null ? {} : { cwd }), - ...(exitCode === null ? {} : { exitCode }), - kind: "terminal", - stderr: [], - stdout: textContent(aggregatedOutput), - }); - } - - if (type === "fileChange") { - const changes = toFileChanges(native); - - return changes.length === 0 && existing?.kind !== "change" - ? null - : itemSchema.parse({ ...base, changes, kind: "change" }); - } - - if (type === "mcpToolCall") { - const server = readNonEmptyString(native, "server"); - const tool = readNonEmptyString(native, "tool"); - - if (server === null || tool === null) { - return null; - } - - const result = readRecord(native, "result"); - const jsonInput = asJsonValue(native["arguments"]); - const output = readArray(result, "content").flatMap((entry) => { - if (isRecord(entry) && readString(entry, "type") === "text") { - const text = readString(entry, "text"); - - if (text !== null) { - return [{ text, type: "text" }]; - } - } - - const value = asJsonValue(entry); - return value === undefined ? [] : [{ type: "json", value }]; - }); - const structuredOutput = asJsonValue(result?.["structuredContent"]); - - return itemSchema.parse({ - ...base, - category: "other", - ...(jsonInput === undefined ? {} : { input: jsonInput }), - kind: "tool", - name: tool, - origin: "mcp", - ...(output.length === 0 ? {} : { output }), - server, - ...(structuredOutput === undefined || structuredOutput === null ? {} : { structuredOutput }), - }); - } - - if (type === "webSearch") { - const query = readString(native, "query"); - - if (query === null) { - return null; - } - - const action = asJsonValue(native["action"]); - const structuredOutput = asJsonValue(native["results"]); - - return itemSchema.parse({ - ...base, - category: "search", - input: { - ...(action === undefined || action === null ? {} : { action }), - query, - }, - kind: "tool", - name: "web_search", - origin: "provider", - ...(structuredOutput === undefined || structuredOutput === null ? {} : { structuredOutput }), - }); - } - - if (type === "plan") { - const text = readString(native, "text"); - - if (text === null) { - return null; - } - - const completed = lifecycle === "completed" || readString(native, "status") === "completed"; - - return itemSchema.parse({ - ...base, - entries: [{ id: "0", status: completed ? "completed" : "pending", text }], - kind: "plan", - }); - } - - if (type === "imageGeneration") { - const result = readNonEmptyString(native, "result"); - const revisedPrompt = readNonEmptyString(native, "revisedPrompt"); - const savedPath = readNonEmptyString(native, "savedPath"); - - return itemSchema.parse({ - ...base, - category: "other", - ...(revisedPrompt === null ? {} : { input: { revisedPrompt } }), - kind: "tool", - ...(savedPath === null ? {} : { locations: [{ path: savedPath }] }), - name: "image_generation", - origin: "provider", - ...(result === null - ? {} - : { output: [{ data: result, mediaType: "image/png", type: "inline_blob" }] }), - }); - } - - if (type === "dynamicToolCall") { - const name = dynamicToolName(native); - - if (name === null) { - return null; - } - - const input = asJsonValue(native["arguments"]); - const output = fromNativeToolContent(native); - - return itemSchema.parse({ - ...base, - category: "other", - ...(input === undefined ? {} : { input }), - kind: "tool", - name, - origin: "provider", - ...(output.length === 0 ? {} : { output }), - }); - } - - if (type === "collabAgentToolCall") { - const name = readNonEmptyString(native, "tool"); - - if (name === null) { - return null; - } - - const input = asJsonValue({ - model: native["model"] ?? null, - prompt: native["prompt"] ?? null, - reasoningEffort: native["reasoningEffort"] ?? null, - receiverThreadIds: readArray(native, "receiverThreadIds"), - senderThreadId: native["senderThreadId"] ?? null, - }); - const structuredOutput = asJsonValue(native["agentsStates"]); - - return itemSchema.parse({ - ...base, - category: "agent", - ...(input === undefined ? {} : { input }), - kind: "tool", - name, - origin: "provider", - ...(structuredOutput === undefined ? {} : { structuredOutput }), - }); - } - - if (type === "subAgentActivity") { - const input = asJsonValue({ - agentPath: native["agentPath"] ?? null, - agentThreadId: native["agentThreadId"] ?? null, - kind: native["kind"] ?? null, - }); - - return itemSchema.parse({ - ...base, - category: "agent", - ...(input === undefined ? {} : { input }), - kind: "tool", - name: "sub_agent_activity", - origin: "provider", - }); - } - - if (type === "imageView" || type === "sleep") { - const name = - readNonEmptyString(native, "tool") ?? - (type === "imageView" ? "image_view" : type === "sleep" ? "sleep" : type); - const input = native["arguments"] ?? native["path"] ?? native["durationMs"]; - const output = native["contentItems"]; - const jsonInput = asJsonValue(input); - const jsonOutput = asJsonValue(output); - - return itemSchema.parse({ - ...base, - category: type === "imageView" ? "read" : "other", - ...(jsonInput === undefined ? {} : { input: jsonInput }), - kind: "tool", - name, - origin: "provider", - ...(jsonOutput !== undefined - ? { output: [{ type: "json", value: jsonOutput }], structuredOutput: jsonOutput } - : {}), - }); - } - - return itemSchema.parse({ - ...base, - kind: "extension", - name: PROVIDER_EXTENSION_ITEM, - value: asJsonValue(native) ?? { nativeType: type }, - }); -} diff --git a/src/runtimes/openai/contract-turn-inbox.ts b/src/runtimes/openai/contract-turn-inbox.ts deleted file mode 100644 index e2753eb..0000000 --- a/src/runtimes/openai/contract-turn-inbox.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; - -import type { JsonObject } from "./app-server-json"; - -const MAX_PENDING_TURN_BYTES = 8 * 1_024 * 1_024; -const MAX_TRACKED_TURNS = 1_024; - -interface PendingTurnEnd { - readonly bytes: number; - readonly params: JsonObject; - task?: Promise | undefined; -} - -interface PendingTurnNotification { - readonly bytes: number; - readonly method: string; - readonly params: JsonObject; -} - -export interface OpenAiContractTurnInboxOptions { - readonly dispatch: (method: string, params: JsonObject) => Promise; - readonly replayEnd: (params: JsonObject) => Promise; -} - -export class OpenAiContractTurnInbox { - readonly #dispatch: OpenAiContractTurnInboxOptions["dispatch"]; - readonly #ended = new Map(); - readonly #pendingEnds = new Map(); - readonly #pendingNotifications = new Map(); - readonly #pendingReplays = new Map>(); - readonly #replayEnd: OpenAiContractTurnInboxOptions["replayEnd"]; - readonly #textEncoder = new TextEncoder(); - #disposed = false; - #pendingEndBytes = 0; - #pendingNotificationBytes = 0; - #pendingNotificationCount = 0; - - constructor(options: OpenAiContractTurnInboxOptions) { - this.#dispatch = options.dispatch; - this.#replayEnd = options.replayEnd; - } - - ended(turnId: string): TTurn | undefined { - return this.#ended.get(turnId); - } - - hasEnded(turnId: string): boolean { - return this.#ended.has(turnId); - } - - shouldBuffer(turnId: string): boolean { - return ( - this.#pendingNotifications.has(turnId) || - this.#pendingEnds.has(turnId) || - this.#pendingReplays.has(turnId) - ); - } - - rememberNotification(turnId: string, method: string, params: JsonObject): void { - if (this.#pendingEnds.has(turnId)) { - throw new Error( - `OpenAI app-server event ${method} arrived after terminal Turn ${turnId} before attachment.`, - ); - } - - const snapshot = structuredClone(params); - const bytes = this.#textEncoder.encode(JSON.stringify(snapshot)).byteLength; - - if ( - this.#pendingNotificationCount >= MAX_TRACKED_TURNS || - bytes > MAX_PENDING_TURN_BYTES - this.#pendingNotificationBytes - ) { - throw new RangeError("OpenAI app-server pending Turn event limit is exhausted."); - } - - const queue = this.#pendingNotifications.get(turnId) ?? []; - queue.push({ bytes, method, params: snapshot }); - this.#pendingNotifications.set(turnId, queue); - this.#pendingNotificationBytes += bytes; - this.#pendingNotificationCount += 1; - } - - rememberEnd(turnId: string, params: JsonObject): void { - const existing = this.#pendingEnds.get(turnId); - - if (existing !== undefined) { - if (!isDeepStrictEqual(existing.params, params)) { - throw new Error(`OpenAI terminal Turn ${turnId} changed before attachment.`); - } - return; - } - - const snapshot = structuredClone(params); - const bytes = this.#textEncoder.encode(JSON.stringify(snapshot)).byteLength; - - if ( - this.#pendingEnds.size >= MAX_TRACKED_TURNS || - bytes > MAX_PENDING_TURN_BYTES - this.#pendingEndBytes - ) { - throw new RangeError("OpenAI app-server pending terminal Turn limit is exhausted."); - } - - this.#pendingEnds.set(turnId, { bytes, params: snapshot }); - this.#pendingEndBytes += bytes; - } - - async replay(turnId: string): Promise { - const existing = this.#pendingReplays.get(turnId); - if (existing !== undefined) { - await existing; - return; - } - - const task = this.#drain(turnId).finally(() => { - if (this.#pendingReplays.get(turnId) === task) { - this.#pendingReplays.delete(turnId); - } - }); - this.#pendingReplays.set(turnId, task); - await task; - } - - rememberEnded(turnId: string, turn: TTurn): void { - this.#ended.delete(turnId); - this.#ended.set(turnId, turn); - - if (this.#ended.size > MAX_TRACKED_TURNS) { - const oldest = this.#ended.keys().next().value; - if (oldest !== undefined) { - this.#ended.delete(oldest); - } - } - } - - dispose(): void { - this.#disposed = true; - this.#ended.clear(); - this.#pendingEnds.clear(); - this.#pendingNotifications.clear(); - this.#pendingReplays.clear(); - this.#pendingEndBytes = 0; - this.#pendingNotificationBytes = 0; - this.#pendingNotificationCount = 0; - } - - async #drain(turnId: string): Promise { - for (;;) { - const queue = this.#pendingNotifications.get(turnId); - const pending = queue?.[0]; - - if (queue !== undefined && pending !== undefined) { - await this.#dispatch(pending.method, pending.params); - if (this.#disposed || this.#pendingNotifications.get(turnId) !== queue) { - return; - } - - queue.shift(); - this.#pendingNotificationBytes -= pending.bytes; - this.#pendingNotificationCount -= 1; - if (queue.length === 0) { - this.#pendingNotifications.delete(turnId); - } - continue; - } - - await this.#replayPendingEnd(turnId); - if (!this.#pendingNotifications.has(turnId) && !this.#pendingEnds.has(turnId)) { - return; - } - } - } - - async #replayPendingEnd(turnId: string): Promise { - const pending = this.#pendingEnds.get(turnId); - if (pending === undefined) { - return; - } - - pending.task ??= this.#replayEnd(pending.params) - .then(() => { - if (this.#pendingEnds.get(turnId) === pending) { - this.#pendingEnds.delete(turnId); - this.#pendingEndBytes -= pending.bytes; - } - }) - .finally(() => { - pending.task = undefined; - }); - await pending.task; - } -} diff --git a/src/runtimes/openai/contract-turn-lifecycle.ts b/src/runtimes/openai/contract-turn-lifecycle.ts deleted file mode 100644 index b57d528..0000000 --- a/src/runtimes/openai/contract-turn-lifecycle.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { itemSchema } from "../../contract"; -import type { Item, ProtocolError } from "../../contract"; -import type { ContractProjection } from "../contract-projection"; -import { isRecord, readArray, readNonEmptyString, readRecord, readString } from "./app-server-json"; -import type { JsonObject } from "./app-server-json"; -import { - latestTimestamp, - type NativeItemLifecycle, - provenance, - providerEventId, -} from "./contract-items"; - -export interface OpenAiTurnLifecycleState { - readonly runId: string; - readonly threadId: string; - readonly turnId: string; -} - -export function projectOpenAiPlan( - turn: OpenAiTurnLifecycleState, - params: JsonObject, - occurredAt: string, - event: string, - previous: Item | undefined, -): Item { - const explanation = readString(params, "explanation"); - - return itemSchema.parse({ - audience: "participants", - createdAt: previous?.createdAt ?? occurredAt, - entries: readArray(params, "plan").flatMap((entry, index) => { - if (!isRecord(entry)) { - return []; - } - - const text = readNonEmptyString(entry, "step"); - const status = readString(entry, "status"); - return text === null - ? [] - : [ - { - id: String(index), - status: - status === "completed" - ? "completed" - : status === "inProgress" - ? "in_progress" - : "pending", - text, - }, - ]; - }), - ...(explanation === null ? {} : { explanation }), - id: "turn-plan", - kind: "plan", - provenance: provenance(event, { - itemId: "turn-plan", - threadId: turn.threadId, - turnId: turn.turnId, - }), - runId: turn.runId, - status: "active", - updatedAt: latestTimestamp(previous?.updatedAt, occurredAt), - }); -} - -export interface FinishOpenAiTurnOptions { - readonly method: string; - readonly params: JsonObject; - readonly projectItem: ( - turn: TTurn, - item: JsonObject, - lifecycle: NativeItemLifecycle, - occurredAt: string, - method: string, - ) => Item | null; - readonly projection: ContractProjection; - readonly release: (turnId: string) => void; - readonly rememberEnded: (turn: TTurn) => void; - readonly turn: TTurn; - readonly withReceiptTime: ( - eventId: string, - operation: (occurredAt: string) => Promise, - ) => Promise; -} - -export async function finishOpenAiTurn( - options: FinishOpenAiTurnOptions, -): Promise { - const { method, params, projection, turn } = options; - const nativeTurn = readRecord(params, "turn"); - const status = readString(nativeTurn, "status"); - - if (status === "inProgress") { - return; - } - - if (status !== "completed" && status !== "failed" && status !== "interrupted") { - throw new Error(`${method} params.turn.status is unsupported.`); - } - - await options.withReceiptTime(providerEventId(method, params), async (endedAt) => { - const terminalItems: Item[] = []; - const completedItemIds = new Set(); - - for (const nativeItem of readArray(nativeTurn, "items")) { - if (!isRecord(nativeItem)) { - continue; - } - - const itemId = readNonEmptyString(nativeItem, "id"); - - if (itemId === null) { - continue; - } - - const existing = projection.item(turn.runId, itemId); - - if (existing !== undefined && existing.status !== "active") { - continue; - } - - const projected = options.projectItem(turn, nativeItem, "completed", endedAt, method); - - if (projected !== null) { - completedItemIds.add(itemId); - terminalItems.push(projected); - } - } - - const activeItems = projection.items(turn.runId).filter((item) => item.status === "active"); - const incompleteSnapshot = - status === "completed" && - activeItems.some( - (item) => - !completedItemIds.has(item.id) && !(item.kind === "plan" && item.id === "turn-plan"), - ); - const runStatus = - status === "failed" || incompleteSnapshot - ? "failed" - : status === "interrupted" - ? "cancelled" - : "completed"; - const runError: ProtocolError = { - code: incompleteSnapshot ? "openai.turn.incomplete" : "openai.turn.failed", - message: incompleteSnapshot - ? "OpenAI turn completed without authoritative snapshots for active items." - : (readString(readRecord(nativeTurn, "error"), "message") ?? "OpenAI turn failed."), - retryable: false, - }; - - if (status === "completed") { - const plan = activeItems.find( - (item) => item.kind === "plan" && item.id === "turn-plan" && !completedItemIds.has(item.id), - ); - - if (plan !== undefined) { - terminalItems.push( - itemSchema.parse({ - ...plan, - endedAt, - status: "completed", - updatedAt: latestTimestamp(plan.updatedAt, endedAt), - }), - ); - } - } - - await projection.finishRun({ - cause: { providerEventId: providerEventId(method, params), type: "provider" }, - event: method, - ...(runStatus === "failed" ? { error: runError } : {}), - ...(runStatus === "completed" ? { finishReason: "success" } : {}), - runId: turn.runId, - status: runStatus, - terminalItems, - }); - options.rememberEnded(turn); - options.release(turn.turnId); - }); -} diff --git a/src/runtimes/openai/event-translator.ts b/src/runtimes/openai/event-translator.ts index 233f14c..3f6d231 100644 --- a/src/runtimes/openai/event-translator.ts +++ b/src/runtimes/openai/event-translator.ts @@ -9,45 +9,104 @@ import { } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; -export function toOpenAiToolName(item: JsonObject): string | null { - const itemType = readString(item, "type"); - - if (itemType === "commandExecution") { - return "Shell"; +function withoutPrivateMeta(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(withoutPrivateMeta); } - if (itemType === "fileChange") { - return "File change"; + if (!isRecord(value)) { + return value; } - if (itemType === "mcpToolCall") { - const server = readNonEmptyString(item, "server"); - const tool = readNonEmptyString(item, "tool"); + return Object.fromEntries( + Object.entries(value).flatMap(([key, entry]) => + key === "_meta" ? [] : [[key, withoutPrivateMeta(entry)]], + ), + ); +} - if (server !== null && tool !== null) { - return `${server}.${tool}`; - } +export function toOpenAiMessagePhase(item: JsonObject): "commentary" | "final" | null { + if (readString(item, "delivery") === "async") return "commentary"; - return tool ?? "MCP tool"; + switch (readString(item, "phase")) { + case "commentary": + return "commentary"; + case "final_answer": + return "final"; + default: + return null; } +} - if (itemType === "dynamicToolCall") { - return readString(item, "tool") ?? "Tool"; - } +export function toOpenAiToolName(item: JsonObject): string | null { + const itemType = readString(item, "type"); - if (itemType === "webSearch") { - return "Web search"; - } + switch (itemType) { + case "commandExecution": + return "Shell"; + case "fileChange": + return "File change"; + case "mcpToolCall": { + const server = readNonEmptyString(item, "server"); + const tool = readNonEmptyString(item, "tool"); - if (itemType === "collabAgentToolCall") { - return readNonEmptyString(item, "tool") ?? "Agent collaboration"; + return server !== null && tool !== null ? `${server}.${tool}` : (tool ?? "MCP tool"); + } + case "dynamicToolCall": + return readString(item, "tool") ?? "Tool"; + case "collabAgentToolCall": + switch (readString(item, "tool")) { + case "spawnAgent": + return "Spawn agent"; + case "sendInput": + return "Send input to agent"; + case "resumeAgent": + return "Resume agent"; + case "wait": + return "Wait for agents"; + case "closeAgent": + return "Close agent"; + case "sendMessage": + return "Send message to agent"; + case "followupTask": + return "Follow up with agent"; + case "interruptAgent": + return "Interrupt agent"; + case "listAgents": + return "List agents"; + default: + return null; + } + case "webSearch": + return "Web search"; + case "imageView": + return "View image"; + case "sleep": + return "Sleep"; + case "imageGeneration": + return "Image generation"; + default: + return null; } +} - if (itemType === "subAgentActivity") { - return "Sub-agent activity"; +export function toOpenAiCollaborationOutput(item: JsonObject): JsonObject | null { + if (readString(item, "type") !== "collabAgentToolCall") { + return null; } - return null; + return { + agentsStates: readRecord(item, "agentsStates") ?? {}, + model: readString(item, "model"), + prompt: readString(item, "prompt"), + reasoningEffort: readString(item, "reasoningEffort"), + receiverThreadIds: readArray(item, "receiverThreadIds").filter( + (entry): entry is string => typeof entry === "string", + ), + senderThreadId: readString(item, "senderThreadId"), + status: readString(item, "status"), + tool: readString(item, "tool"), + }; } export function toOpenAiToolResultText(item: JsonObject): string | null { @@ -58,25 +117,9 @@ export function toOpenAiToolResultText(item: JsonObject): string | null { } if (itemType === "fileChange") { - const changes = readArray(item, "changes"); - if (changes.length === 0) { - return null; - } - - return changes - .map((change) => { - if (!isRecord(change)) { - return null; - } - - const path = readNonEmptyString(change, "path"); - const diff = readNonEmptyString(change, "diff"); - return [path, diff] - .filter((entry): entry is string => entry !== null && entry.length > 0) - .join("\n"); - }) - .filter((entry): entry is string => entry !== null && entry.length > 0) - .join("\n\n"); + // file.change.updated is the durable authority for file paths. Repeating + // provider diffs here can exceed CMA admission without adding information. + return null; } if (itemType === "mcpToolCall") { @@ -85,32 +128,86 @@ export function toOpenAiToolResultText(item: JsonObject): string | null { return readString(error, "message") ?? "MCP tool failed."; } - return stringifyForDisplay(item["result"]); + return stringifyForDisplay(withoutPrivateMeta(readRecord(item, "result")?.["content"])); } if (itemType === "dynamicToolCall") { return stringifyForDisplay(item["contentItems"] ?? item["success"]); } + if (itemType === "collabAgentToolCall") { + return stringifyForDisplay(toOpenAiCollaborationOutput(item)); + } + if (itemType === "webSearch") { return readString(item, "query"); } - if (itemType === "collabAgentToolCall") { - return stringifyForDisplay(item["agentsStates"]); + if (itemType === "sleep") { + const durationMs = item["durationMs"]; + return typeof durationMs === "number" && Number.isSafeInteger(durationMs) + ? `Slept for ${String(durationMs)} ms.` + : null; } - if (itemType === "subAgentActivity") { - return stringifyForDisplay({ - agentPath: item["agentPath"] ?? null, - agentThreadId: item["agentThreadId"] ?? null, - kind: item["kind"] ?? null, - }); + if (itemType === "imageView") { + return readString(item, "path"); + } + + if (itemType === "imageGeneration") { + if (readString(item, "status") === "failed") { + return "Image generation failed."; + } + + return "Image generated."; } return null; } +export function toOpenAiToolRawInput(item: JsonObject): string | null { + switch (readString(item, "type")) { + case "commandExecution": + return readString(item, "command"); + case "mcpToolCall": + case "dynamicToolCall": + return item["arguments"] === undefined ? null : (JSON.stringify(item["arguments"]) ?? null); + default: + return null; + } +} + +export function toOpenAiToolStructuredOutput(item: JsonObject): unknown | null { + switch (readString(item, "type")) { + case "commandExecution": + return { + commandActions: item["commandActions"] ?? [], + cwd: item["cwd"] ?? null, + durationMs: item["durationMs"] ?? null, + exitCode: item["exitCode"] ?? null, + pluginId: item["pluginId"] ?? null, + processId: item["processId"] ?? null, + scriptPath: item["scriptPath"] ?? null, + source: item["source"] ?? null, + }; + case "mcpToolCall": + return withoutPrivateMeta(readRecord(item, "result")?.["structuredContent"] ?? null); + case "dynamicToolCall": + return { + contentItems: item["contentItems"] ?? null, + durationMs: item["durationMs"] ?? null, + namespace: item["namespace"] ?? null, + success: item["success"] ?? null, + }; + case "imageGeneration": { + const failure = readRecord(item, "failure"); + return failure === null ? null : { failure }; + } + default: + return null; + } +} + export function toOpenAiFileChangeEvents(item: JsonObject): DriverEventInput[] { if (readString(item, "type") !== "fileChange") { return []; @@ -129,18 +226,22 @@ export function toOpenAiFileChangeEvents(item: JsonObject): DriverEventInput[] { return []; } + const movePath = changeType === "update" ? readNonEmptyString(kind, "move_path") : null; + const changes = + movePath === null || movePath === path + ? [{ change: changeType === "delete" ? ("delete" as const) : ("upsert" as const), path }] + : [ + { change: "delete" as const, path }, + { change: "upsert" as const, path: movePath }, + ]; + return [ { actor: "tool", kind: "file.change.updated", origin: "file", payload: { - changes: [ - { - change: changeType === "delete" ? "delete" : "upsert", - path, - }, - ], + changes, status: "completed", }, }, diff --git a/src/runtimes/openai/generated-json-schema/InitializeResponse.json b/src/runtimes/openai/generated-json-schema/InitializeResponse.json new file mode 100644 index 0000000..795eae0 --- /dev/null +++ b/src/runtimes/openai/generated-json-schema/InitializeResponse.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + } + }, + "properties": { + "codexHome": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to the server's $CODEX_HOME directory." + }, + "platformFamily": { + "description": "Platform family for the running app-server target, for example `\"unix\"` or `\"windows\"`.", + "type": "string" + }, + "platformOs": { + "description": "Operating system for the running app-server target, for example `\"macos\"`, `\"linux\"`, or `\"windows\"`.", + "type": "string" + }, + "userAgent": { + "type": "string" + } + }, + "required": ["codexHome", "platformFamily", "platformOs", "userAgent"], + "title": "InitializeResponse", + "type": "object" +} diff --git a/src/runtimes/openai/generated-json-schema/README.md b/src/runtimes/openai/generated-json-schema/README.md new file mode 100644 index 0000000..5480a44 --- /dev/null +++ b/src/runtimes/openai/generated-json-schema/README.md @@ -0,0 +1,21 @@ +# OpenAI app-server JSON schemas + +These eight runtime schemas are selected from `@openai/codex@0.150.1` output and must not be edited by hand. + +```sh +vp exec codex --version +schema_tmp=$(mktemp -d) +trap 'rm -rf "$schema_tmp"' EXIT +schema_dir=src/runtimes/openai/generated-json-schema +vp exec codex app-server generate-json-schema --experimental --out "$schema_tmp" +fd --extension json . "$schema_dir" --exec rm +cp "$schema_tmp/ServerNotification.json" "$schema_dir/ServerNotification.json" +cp "$schema_tmp/ServerRequest.json" "$schema_dir/ServerRequest.json" +cp "$schema_tmp/v1/InitializeResponse.json" "$schema_dir/InitializeResponse.json" +cp "$schema_tmp/v2/ThreadBackgroundTerminalsCleanResponse.json" "$schema_dir/ThreadBackgroundTerminalsCleanResponse.json" +cp "$schema_tmp/v2/ThreadInjectItemsResponse.json" "$schema_dir/ThreadInjectItemsResponse.json" +cp "$schema_tmp/v2/ThreadStartResponse.json" "$schema_dir/ThreadStartResponse.json" +cp "$schema_tmp/v2/ThreadResumeResponse.json" "$schema_dir/ThreadResumeResponse.json" +cp "$schema_tmp/v2/TurnStartResponse.json" "$schema_dir/TurnStartResponse.json" +vp fmt "$schema_dir" +``` diff --git a/src/runtimes/openai/generated-json-schema/ServerNotification.json b/src/runtimes/openai/generated-json-schema/ServerNotification.json new file mode 100644 index 0000000..f7ae199 --- /dev/null +++ b/src/runtimes/openai/generated-json-schema/ServerNotification.json @@ -0,0 +1,6231 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "AccountLoginCompletedNotification": { + "properties": { + "error": { + "type": ["string", "null"] + }, + "loginId": { + "type": ["string", "null"] + }, + "onboardingEntrypoint": { + "anyOf": [ + { + "$ref": "#/definitions/DesktopOnboardingEntrypoint" + }, + { + "type": "null" + } + ] + }, + "success": { + "type": "boolean" + } + }, + "required": ["success"], + "type": "object" + }, + "AccountRateLimitsUpdatedNotification": { + "description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.", + "properties": { + "rateLimits": { + "$ref": "#/definitions/RateLimitSnapshot" + } + }, + "required": ["rateLimits"], + "type": "object" + }, + "AccountUpdatedNotification": { + "properties": { + "authMode": { + "anyOf": [ + { + "$ref": "#/definitions/AuthMode" + }, + { + "type": "null" + } + ] + }, + "planType": { + "anyOf": [ + { + "$ref": "#/definitions/PlanType" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ActivePermissionProfile": { + "properties": { + "extends": { + "default": null, + "description": "Parent profile identifier from the selected permissions profile's `extends` setting, when present.", + "type": ["string", "null"] + }, + "id": { + "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", + "type": "string" + } + }, + "required": ["id"], + "type": "object" + }, + "AdditionalFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": ["array", "null"] + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": ["integer", "null"] + }, + "read": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": ["array", "null"] + }, + "write": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": ["array", "null"] + } + }, + "type": "object" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": ["boolean", "null"] + } + }, + "type": "object" + }, + "AgentMessageDelivery": { + "enum": ["async"], + "type": "string" + }, + "AgentMessageDeltaNotification": { + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["delta", "itemId", "threadId", "turnId"], + "type": "object" + }, + "AgentPath": { + "type": "string" + }, + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": ["string", "null"] + }, + "developer": { + "type": ["string", "null"] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": ["string", "null"] + }, + "termsOfService": { + "type": ["string", "null"] + }, + "website": { + "type": ["string", "null"] + } + }, + "required": ["isDiscoverableApp"], + "type": "object" + }, + "AppInfo": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/AppBranding" + }, + { + "type": "null" + } + ] + }, + "description": { + "type": ["string", "null"] + }, + "distributionChannel": { + "type": ["string", "null"] + }, + "iconAssets": { + "additionalProperties": { + "type": "string" + }, + "type": ["object", "null"] + }, + "iconDarkAssets": { + "additionalProperties": { + "type": "string" + }, + "type": ["object", "null"] + }, + "id": { + "type": "string" + }, + "installUrl": { + "type": ["string", "null"] + }, + "isAccessible": { + "default": false, + "type": "boolean" + }, + "isEnabled": { + "default": true, + "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", + "type": "boolean" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": ["object", "null"] + }, + "logoUrl": { + "type": ["string", "null"] + }, + "logoUrlDark": { + "type": ["string", "null"] + }, + "name": { + "type": "string" + }, + "pluginDisplayNames": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["id", "name"], + "type": "object" + }, + "AppListUpdatedNotification": { + "description": "EXPERIMENTAL - notification emitted when the app list changes.", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/AppInfo" + }, + "type": "array" + } + }, + "required": ["data"], + "type": "object" + }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "developer": { + "type": ["string", "null"] + }, + "firstPartyRequiresInstall": { + "type": ["boolean", "null"] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/AppScreenshot" + }, + "type": ["array", "null"] + }, + "seoDescription": { + "type": ["string", "null"] + }, + "showInComposerWhenUnlinked": { + "type": ["boolean", "null"] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "version": { + "type": ["string", "null"] + }, + "versionId": { + "type": ["string", "null"] + }, + "versionNotes": { + "type": ["string", "null"] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": ["status"], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": ["string", "null"] + }, + "url": { + "type": ["string", "null"] + }, + "userPrompt": { + "type": "string" + } + }, + "required": ["userPrompt"], + "type": "object" + }, + "ApprovalsReviewer": { + "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", + "enum": ["user", "auto_review", "guardian_subagent"], + "type": "string" + }, + "AskForApproval": { + "oneOf": [ + { + "enum": ["untrusted", "on-request", "never"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "granular": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } + }, + "required": ["mcp_elicitations", "rules", "sandbox_approval"], + "type": "object" + } + }, + "required": ["granular"], + "title": "GranularAskForApproval", + "type": "object" + } + ] + }, + "AuthMode": { + "description": "Authentication mode for OpenAI-backed providers.", + "oneOf": [ + { + "description": "OpenAI API key provided by the caller and stored by Codex.", + "enum": ["apikey"], + "type": "string" + }, + { + "description": "ChatGPT OAuth managed by Codex (tokens persisted and refreshed by Codex).", + "enum": ["chatgpt"], + "type": "string" + }, + { + "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nChatGPT auth tokens are supplied by an external host app and are only stored in memory. Token refresh must be handled by the external host app.", + "enum": ["chatgptAuthTokens"], + "type": "string" + }, + { + "description": "Backend auth supplied as request headers.", + "enum": ["headers"], + "type": "string" + }, + { + "description": "Programmatic Codex auth backed by a registered Agent Identity.", + "enum": ["agentIdentity"], + "type": "string" + }, + { + "description": "Programmatic Codex auth backed by a personal access token.", + "enum": ["personalAccessToken"], + "type": "string" + }, + { + "description": "Amazon Bedrock bearer token managed by Codex.", + "enum": ["bedrockApiKey"], + "type": "string" + }, + { + "description": "Amazon Bedrock AWS access keys managed by Codex.", + "enum": ["bedrockAccessKeys"], + "type": "string" + } + ] + }, + "AutoReviewDecisionSource": { + "description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.", + "enum": ["agent"], + "type": "string" + }, + "ByteRange": { + "properties": { + "end": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "start": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": ["end", "start"], + "type": "object" + }, + "CodexErrorInfo": { + "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", + "oneOf": [ + { + "enum": [ + "contextWindowExceeded", + "sessionBudgetExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "misalignmentPolicyViolation", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "httpConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["httpConnectionFailed"], + "title": "HttpConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Failed to connect to the response SSE stream.", + "properties": { + "responseStreamConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseStreamConnectionFailed"], + "title": "ResponseStreamConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "The response SSE stream disconnected in the middle of a turn before completion.", + "properties": { + "responseStreamDisconnected": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseStreamDisconnected"], + "title": "ResponseStreamDisconnectedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reached the retry limit for responses.", + "properties": { + "responseTooManyFailedAttempts": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseTooManyFailedAttempts"], + "title": "ResponseTooManyFailedAttemptsCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", + "properties": { + "activeTurnNotSteerable": { + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + }, + "required": ["turnKind"], + "type": "object" + } + }, + "required": ["activeTurnNotSteerable"], + "title": "ActiveTurnNotSteerableCodexErrorInfo", + "type": "object" + } + ] + }, + "CollabAgentState": { + "properties": { + "message": { + "type": ["string", "null"] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } + }, + "required": ["status"], + "type": "object" + }, + "CollabAgentStatus": { + "enum": [ + "pendingInit", + "running", + "interrupted", + "completed", + "errored", + "shutdown", + "notFound" + ], + "type": "string" + }, + "CollabAgentTool": { + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents" + ], + "type": "string" + }, + "CollabAgentToolCallStatus": { + "enum": ["inProgress", "completed", "failed", "interrupted"], + "type": "string" + }, + "CollaborationMode": { + "description": "Collaboration mode for a Codex session.", + "properties": { + "mode": { + "$ref": "#/definitions/ModeKind" + }, + "settings": { + "$ref": "#/definitions/Settings" + } + }, + "required": ["mode", "settings"], + "type": "object" + }, + "CommandAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["read"], + "title": "ReadCommandActionType", + "type": "string" + } + }, + "required": ["command", "name", "path", "type"], + "title": "ReadCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["listFiles"], + "title": "ListFilesCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "ListFilesCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "SearchCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "type": { + "enum": ["unknown"], + "title": "UnknownCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "UnknownCommandAction", + "type": "object" + } + ] + }, + "CommandExecOutputDeltaNotification": { + "description": "Base64-encoded output chunk emitted for a streaming `command/exec` request.\n\nThese notifications are connection-scoped. If the originating connection closes, the server terminates the process.", + "properties": { + "capReached": { + "description": "`true` on the final streamed chunk for a stream when `outputBytesCap` truncated later output on that stream.", + "type": "boolean" + }, + "deltaBase64": { + "description": "Base64-encoded output bytes.", + "type": "string" + }, + "processId": { + "description": "Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + "type": "string" + }, + "stream": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecOutputStream" + } + ], + "description": "Output stream for this chunk." + } + }, + "required": ["capReached", "deltaBase64", "processId", "stream"], + "type": "object" + }, + "CommandExecOutputStream": { + "description": "Stream label for `command/exec/outputDelta` notifications.", + "oneOf": [ + { + "description": "stdout stream. PTY mode multiplexes terminal output here.", + "enum": ["stdout"], + "type": "string" + }, + { + "description": "stderr stream.", + "enum": ["stderr"], + "type": "string" + } + ] + }, + "CommandExecutionOutputDeltaNotification": { + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["delta", "itemId", "threadId", "turnId"], + "type": "object" + }, + "CommandExecutionSource": { + "enum": ["agent", "userShell", "unifiedExecStartup", "unifiedExecInteraction"], + "type": "string" + }, + "CommandExecutionStatus": { + "enum": ["inProgress", "completed", "failed", "declined"], + "type": "string" + }, + "ConfigWarningNotification": { + "properties": { + "details": { + "description": "Optional extra guidance or error details.", + "type": ["string", "null"] + }, + "path": { + "description": "Optional path to the config file that triggered the warning.", + "type": ["string", "null"] + }, + "range": { + "anyOf": [ + { + "$ref": "#/definitions/TextRange" + }, + { + "type": "null" + } + ], + "description": "Optional range for the error location inside the config file." + }, + "summary": { + "description": "Concise summary of the warning.", + "type": "string" + } + }, + "required": ["summary"], + "type": "object" + }, + "ContextCompactedNotification": { + "description": "Deprecated: Use `ContextCompaction` item type instead.", + "properties": { + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["threadId", "turnId"], + "type": "object" + }, + "CreditsSnapshot": { + "properties": { + "balance": { + "type": ["string", "null"] + }, + "hasCredits": { + "type": "boolean" + }, + "unlimited": { + "type": "boolean" + } + }, + "required": ["hasCredits", "unlimited"], + "type": "object" + }, + "DeprecationNoticeNotification": { + "properties": { + "details": { + "description": "Optional extra guidance, such as migration steps or rationale.", + "type": ["string", "null"] + }, + "summary": { + "description": "Concise summary of what is deprecated.", + "type": "string" + } + }, + "required": ["summary"], + "type": "object" + }, + "DesktopOnboardingEntrypoint": { + "enum": ["life_sciences"], + "type": "string" + }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": ["inputText"], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": ["inputImage"], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["imageUrl", "type"], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audioUrl": { + "type": "string" + }, + "type": { + "enum": ["inputAudio"], + "title": "InputAudioDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["audioUrl", "type"], + "title": "InputAudioDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": ["inProgress", "completed", "failed"], + "type": "string" + }, + "EnvironmentConnectionNotification": { + "properties": { + "environmentId": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": ["environmentId", "threadId"], + "type": "object" + }, + "ErrorNotification": { + "properties": { + "error": { + "$ref": "#/definitions/TurnError" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + }, + "willRetry": { + "type": "boolean" + } + }, + "required": ["error", "threadId", "turnId", "willRetry"], + "type": "object" + }, + "ExternalAgentConfigImportCompletedNotification": { + "properties": { + "importId": { + "type": "string" + }, + "itemTypeResults": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportTypeResult" + }, + "type": "array" + } + }, + "required": ["importId", "itemTypeResults"], + "type": "object" + }, + "ExternalAgentConfigImportItemTypeFailure": { + "properties": { + "cwd": { + "type": ["string", "null"] + }, + "errorType": { + "type": ["string", "null"] + }, + "failureStage": { + "type": "string" + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + }, + "message": { + "type": "string" + }, + "source": { + "type": ["string", "null"] + }, + "subErrorType": { + "type": ["string", "null"] + } + }, + "required": ["failureStage", "itemType", "message"], + "type": "object" + }, + "ExternalAgentConfigImportItemTypeSuccess": { + "properties": { + "cwd": { + "type": ["string", "null"] + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + }, + "source": { + "type": ["string", "null"] + }, + "target": { + "type": ["string", "null"] + }, + "title": { + "default": null, + "description": "Original title for an imported session; null for other item types.", + "type": ["string", "null"] + } + }, + "required": ["itemType"], + "type": "object" + }, + "ExternalAgentConfigImportProgressNotification": { + "properties": { + "importId": { + "type": "string" + }, + "itemTypeResults": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportTypeResult" + }, + "type": "array" + } + }, + "required": ["importId", "itemTypeResults"], + "type": "object" + }, + "ExternalAgentConfigImportTypeResult": { + "properties": { + "failures": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportItemTypeFailure" + }, + "type": "array" + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + }, + "successes": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportItemTypeSuccess" + }, + "type": "array" + } + }, + "required": ["failures", "itemType", "successes"], + "type": "object" + }, + "ExternalAgentConfigMigrationItemType": { + "enum": [ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "MEMORY", + "SESSIONS" + ], + "type": "string" + }, + "FileChangeOutputDeltaNotification": { + "description": "Deprecated legacy notification for `apply_patch` textual output.\n\nThe server no longer emits this notification.", + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["delta", "itemId", "threadId", "turnId"], + "type": "object" + }, + "FileChangePatchUpdatedNotification": { + "properties": { + "changes": { + "items": { + "$ref": "#/definitions/FileUpdateChange" + }, + "type": "array" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["changes", "itemId", "threadId", "turnId"], + "type": "object" + }, + "FileSystemAccessMode": { + "enum": ["read", "write", "deny"], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["path"], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": ["glob_pattern"], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": ["pattern", "type"], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["special"], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": ["type", "value"], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": ["access", "path"], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": ["root"], + "type": "string" + } + }, + "required": ["kind"], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["minimal"], + "type": "string" + } + }, + "required": ["kind"], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["project_roots"], + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": ["kind"], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["tmpdir"], + "type": "string" + } + }, + "required": ["kind"], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["slash_tmp"], + "type": "string" + } + }, + "required": ["kind"], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["unknown"], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": ["kind", "path"], + "type": "object" + } + ] + }, + "FileUpdateChange": { + "properties": { + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } + }, + "required": ["diff", "kind", "path"], + "type": "object" + }, + "FsChangedNotification": { + "description": "Filesystem watch notification emitted for `fs/watch` subscribers.", + "properties": { + "changedPaths": { + "description": "File or directory paths associated with this event.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "watchId": { + "description": "Watch identifier previously provided to `fs/watch`.", + "type": "string" + } + }, + "required": ["changedPaths", "watchId"], + "type": "object" + }, + "FuzzyFileSearchMatchType": { + "enum": ["file", "directory"], + "type": "string" + }, + "FuzzyFileSearchResult": { + "description": "Superset of [`codex_file_search::FileMatch`]", + "properties": { + "file_name": { + "type": "string" + }, + "indices": { + "items": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "type": ["array", "null"] + }, + "match_type": { + "$ref": "#/definitions/FuzzyFileSearchMatchType" + }, + "path": { + "type": "string" + }, + "root": { + "type": "string" + }, + "score": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "required": ["file_name", "match_type", "path", "root", "score"], + "type": "object" + }, + "FuzzyFileSearchSessionCompletedNotification": { + "properties": { + "sessionId": { + "type": "string" + } + }, + "required": ["sessionId"], + "type": "object" + }, + "FuzzyFileSearchSessionUpdatedNotification": { + "properties": { + "files": { + "items": { + "$ref": "#/definitions/FuzzyFileSearchResult" + }, + "type": "array" + }, + "query": { + "type": "string" + }, + "sessionId": { + "type": "string" + } + }, + "required": ["files", "query", "sessionId"], + "type": "object" + }, + "GitInfo": { + "properties": { + "branch": { + "type": ["string", "null"] + }, + "originUrl": { + "type": ["string", "null"] + }, + "sha": { + "type": ["string", "null"] + } + }, + "type": "object" + }, + "GuardianApprovalReview": { + "description": "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.", + "properties": { + "rationale": { + "type": ["string", "null"] + }, + "riskLevel": { + "anyOf": [ + { + "$ref": "#/definitions/GuardianRiskLevel" + }, + { + "type": "null" + } + ] + }, + "status": { + "$ref": "#/definitions/GuardianApprovalReviewStatus" + }, + "userAuthorization": { + "anyOf": [ + { + "$ref": "#/definitions/GuardianUserAuthorization" + }, + { + "type": "null" + } + ] + } + }, + "required": ["status"], + "type": "object" + }, + "GuardianApprovalReviewAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "source": { + "$ref": "#/definitions/GuardianCommandSource" + }, + "type": { + "enum": ["command"], + "title": "CommandGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": ["command", "cwd", "source", "type"], + "title": "CommandGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "argv": { + "items": { + "type": "string" + }, + "type": "array" + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "program": { + "type": "string" + }, + "source": { + "$ref": "#/definitions/GuardianCommandSource" + }, + "type": { + "enum": ["execve"], + "title": "ExecveGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": ["argv", "cwd", "program", "source", "type"], + "title": "ExecveGuardianApprovalReviewAction", + "type": "object" + }, + { + "description": "A child approval for input to an existing command execution item.", + "properties": { + "approvalId": { + "type": "string" + }, + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "processId": { + "type": "string" + }, + "stdin": { + "type": "string" + }, + "type": { + "enum": ["writeStdin"], + "title": "WriteStdinGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": ["approvalId", "cwd", "processId", "stdin", "type"], + "title": "WriteStdinGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "files": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": ["applyPatch"], + "title": "ApplyPatchGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": ["cwd", "files", "type"], + "title": "ApplyPatchGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "host": { + "type": "string" + }, + "port": { + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "protocol": { + "$ref": "#/definitions/NetworkApprovalProtocol" + }, + "target": { + "type": "string" + }, + "type": { + "enum": ["networkAccess"], + "title": "NetworkAccessGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": ["host", "port", "protocol", "target", "type"], + "title": "NetworkAccessGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "connectorId": { + "type": ["string", "null"] + }, + "connectorName": { + "type": ["string", "null"] + }, + "server": { + "type": "string" + }, + "toolName": { + "type": "string" + }, + "toolTitle": { + "type": ["string", "null"] + }, + "type": { + "enum": ["mcpToolCall"], + "title": "McpToolCallGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": ["server", "toolName", "type"], + "title": "McpToolCallGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "permissions": { + "$ref": "#/definitions/RequestPermissionProfile" + }, + "reason": { + "type": ["string", "null"] + }, + "type": { + "enum": ["requestPermissions"], + "title": "RequestPermissionsGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": ["permissions", "type"], + "title": "RequestPermissionsGuardianApprovalReviewAction", + "type": "object" + } + ] + }, + "GuardianApprovalReviewStatus": { + "description": "[UNSTABLE] Lifecycle state for an approval auto-review.", + "enum": ["inProgress", "approved", "denied", "timedOut", "aborted"], + "type": "string" + }, + "GuardianCommandSource": { + "enum": ["shell", "unifiedExec"], + "type": "string" + }, + "GuardianRiskLevel": { + "description": "[UNSTABLE] Risk level assigned by approval auto-review.", + "enum": ["low", "medium", "high", "critical"], + "type": "string" + }, + "GuardianUserAuthorization": { + "description": "[UNSTABLE] Authorization level assigned by approval auto-review.", + "enum": ["unknown", "low", "medium", "high"], + "type": "string" + }, + "GuardianWarningNotification": { + "properties": { + "message": { + "description": "Concise guardian warning message for the user.", + "type": "string" + }, + "threadId": { + "description": "Thread target for the guardian warning.", + "type": "string" + } + }, + "required": ["message", "threadId"], + "type": "object" + }, + "HookCompletedNotification": { + "properties": { + "run": { + "$ref": "#/definitions/HookRunSummary" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": ["string", "null"] + } + }, + "required": ["run", "threadId"], + "type": "object" + }, + "HookEventName": { + "enum": [ + "preToolUse", + "permissionRequest", + "postToolUse", + "preCompact", + "postCompact", + "sessionStart", + "sessionEnd", + "userPromptSubmit", + "subagentStart", + "subagentStop", + "stop", + "interrupt" + ], + "type": "string" + }, + "HookExecutionMode": { + "enum": ["sync", "async"], + "type": "string" + }, + "HookHandlerType": { + "enum": ["command", "mcpTool", "prompt", "agent"], + "type": "string" + }, + "HookOutputEntry": { + "properties": { + "kind": { + "$ref": "#/definitions/HookOutputEntryKind" + }, + "text": { + "type": "string" + } + }, + "required": ["kind", "text"], + "type": "object" + }, + "HookOutputEntryKind": { + "enum": ["warning", "stop", "feedback", "context", "error"], + "type": "string" + }, + "HookPromptFragment": { + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["hookRunId", "text"], + "type": "object" + }, + "HookRunStatus": { + "enum": ["running", "completed", "failed", "blocked", "stopped"], + "type": "string" + }, + "HookRunSummary": { + "properties": { + "completedAt": { + "format": "int64", + "type": ["integer", "null"] + }, + "displayOrder": { + "format": "int64", + "type": "integer" + }, + "durationMs": { + "format": "int64", + "type": ["integer", "null"] + }, + "entries": { + "items": { + "$ref": "#/definitions/HookOutputEntry" + }, + "type": "array" + }, + "eventName": { + "$ref": "#/definitions/HookEventName" + }, + "executionMode": { + "$ref": "#/definitions/HookExecutionMode" + }, + "handlerType": { + "$ref": "#/definitions/HookHandlerType" + }, + "id": { + "type": "string" + }, + "scope": { + "$ref": "#/definitions/HookScope" + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/HookSource" + } + ], + "default": "unknown" + }, + "sourcePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "startedAt": { + "format": "int64", + "type": "integer" + }, + "status": { + "$ref": "#/definitions/HookRunStatus" + }, + "statusMessage": { + "type": ["string", "null"] + } + }, + "required": [ + "displayOrder", + "entries", + "eventName", + "executionMode", + "handlerType", + "id", + "scope", + "sourcePath", + "startedAt", + "status" + ], + "type": "object" + }, + "HookScope": { + "enum": ["thread", "turn"], + "type": "string" + }, + "HookSource": { + "enum": [ + "system", + "user", + "project", + "mdm", + "sessionFlags", + "plugin", + "cloudRequirements", + "cloudManagedConfig", + "legacyManagedConfigFile", + "legacyManagedConfigMdm", + "unknown" + ], + "type": "string" + }, + "HookStartedNotification": { + "properties": { + "run": { + "$ref": "#/definitions/HookRunSummary" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": ["string", "null"] + } + }, + "required": ["run", "threadId"], + "type": "object" + }, + "ImageDetail": { + "enum": ["auto", "low", "high", "original"], + "type": "string" + }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": ["integer", "null"] + }, + "type": { + "enum": ["usageLimitExceeded"], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": ["limitId", "type"], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, + "ItemCompletedNotification": { + "properties": { + "completedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle completed.", + "format": "int64", + "type": "integer" + }, + "item": { + "$ref": "#/definitions/ThreadItem" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["completedAtMs", "item", "threadId", "turnId"], + "type": "object" + }, + "ItemGuardianApprovalReviewCompletedNotification": { + "description": "[UNSTABLE] Temporary notification payload for approval auto-review. This shape is expected to change soon.", + "properties": { + "action": { + "$ref": "#/definitions/GuardianApprovalReviewAction" + }, + "completedAtMs": { + "description": "Unix timestamp (in milliseconds) when this review completed.", + "format": "int64", + "type": "integer" + }, + "decisionSource": { + "$ref": "#/definitions/AutoReviewDecisionSource" + }, + "review": { + "$ref": "#/definitions/GuardianApprovalReview" + }, + "reviewId": { + "description": "Stable identifier for this review.", + "type": "string" + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this review started.", + "format": "int64", + "type": "integer" + }, + "targetItemId": { + "description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - stdin reviews, which refer to the existing parent command item and have a separate approval ID in the action payload - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.", + "type": ["string", "null"] + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "action", + "completedAtMs", + "decisionSource", + "review", + "reviewId", + "startedAtMs", + "threadId", + "turnId" + ], + "type": "object" + }, + "ItemGuardianApprovalReviewStartedNotification": { + "description": "[UNSTABLE] Temporary notification payload for approval auto-review. This shape is expected to change soon.", + "properties": { + "action": { + "$ref": "#/definitions/GuardianApprovalReviewAction" + }, + "review": { + "$ref": "#/definitions/GuardianApprovalReview" + }, + "reviewId": { + "description": "Stable identifier for this review.", + "type": "string" + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this review started.", + "format": "int64", + "type": "integer" + }, + "targetItemId": { + "description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - stdin reviews, which refer to the existing parent command item and have a separate approval ID in the action payload - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.", + "type": ["string", "null"] + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["action", "review", "reviewId", "startedAtMs", "threadId", "turnId"], + "type": "object" + }, + "ItemStartedNotification": { + "properties": { + "item": { + "$ref": "#/definitions/ThreadItem" + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["item", "startedAtMs", "threadId", "turnId"], + "type": "object" + }, + "LegacyAppPathString": { + "type": "string" + }, + "McpServerEventNotification": { + "properties": { + "method": { + "type": "string" + }, + "params": true + }, + "required": ["method", "params"], + "type": "object" + }, + "McpServerEventStreamNotification": { + "properties": { + "notification": { + "$ref": "#/definitions/McpServerEventNotification" + }, + "subscriptionId": { + "type": "string" + } + }, + "required": ["notification", "subscriptionId"], + "type": "object" + }, + "McpServerOauthLoginCompletedNotification": { + "properties": { + "error": { + "type": ["string", "null"] + }, + "name": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "threadId": { + "type": ["string", "null"] + } + }, + "required": ["name", "success"], + "type": "object" + }, + "McpServerStartupFailureReason": { + "enum": ["reauthenticationRequired"], + "type": "string" + }, + "McpServerStartupState": { + "enum": ["starting", "ready", "failed", "cancelled"], + "type": "string" + }, + "McpServerStatusUpdatedNotification": { + "properties": { + "error": { + "type": ["string", "null"] + }, + "failureReason": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerStartupFailureReason" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpServerStartupState" + }, + "threadId": { + "type": ["string", "null"] + } + }, + "required": ["name", "status"], + "type": "object" + }, + "McpToolCallAppContext": { + "properties": { + "actionName": { + "type": ["string", "null"] + }, + "appName": { + "type": ["string", "null"] + }, + "connectorId": { + "type": "string" + }, + "linkId": { + "type": ["string", "null"] + }, + "resourceUri": { + "type": ["string", "null"] + } + }, + "required": ["connectorId"], + "type": "object" + }, + "McpToolCallError": { + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, + "McpToolCallProgressNotification": { + "properties": { + "itemId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["itemId", "message", "threadId", "turnId"], + "type": "object" + }, + "McpToolCallResult": { + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "structuredContent": true + }, + "required": ["content"], + "type": "object" + }, + "McpToolCallStatus": { + "enum": ["inProgress", "completed", "failed"], + "type": "string" + }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "threadIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["entries", "threadIds"], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": ["lineEnd", "lineStart", "note", "path"], + "type": "object" + }, + "MessagePhase": { + "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", + "oneOf": [ + { + "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", + "enum": ["commentary"], + "type": "string" + }, + { + "description": "The assistant's terminal answer text for the current turn.", + "enum": ["final_answer"], + "type": "string" + } + ] + }, + "ModeKind": { + "description": "Initial collaboration mode to use when the TUI starts.", + "enum": ["plan", "default"], + "type": "string" + }, + "ModelRerouteReason": { + "enum": ["highRiskCyberActivity"], + "type": "string" + }, + "ModelReroutedNotification": { + "properties": { + "fromModel": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason" + }, + "threadId": { + "type": "string" + }, + "toModel": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["fromModel", "reason", "threadId", "toModel", "turnId"], + "type": "object" + }, + "ModelSafetyBufferingUpdatedNotification": { + "properties": { + "fasterModel": { + "type": ["string", "null"] + }, + "model": { + "type": "string" + }, + "reasons": { + "items": { + "type": "string" + }, + "type": "array" + }, + "showBufferingUi": { + "type": "boolean" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + }, + "useCases": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["model", "reasons", "showBufferingUi", "threadId", "turnId", "useCases"], + "type": "object" + }, + "ModelVerification": { + "enum": ["trustedAccessForCyber"], + "type": "string" + }, + "ModelVerificationNotification": { + "properties": { + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + }, + "verifications": { + "items": { + "$ref": "#/definitions/ModelVerification" + }, + "type": "array" + } + }, + "required": ["threadId", "turnId", "verifications"], + "type": "object" + }, + "MultiAgentMode": { + "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", + "oneOf": [ + { + "enum": ["explicitRequestOnly", "proactive"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "custom": { + "type": "string" + } + }, + "required": ["custom"], + "title": "CustomMultiAgentMode", + "type": "object" + } + ] + }, + "NetworkAccess": { + "enum": ["restricted", "enabled"], + "type": "string" + }, + "NetworkApprovalProtocol": { + "enum": ["http", "https", "socks5Tcp", "socks5Udp"], + "type": "string" + }, + "NonSteerableTurnKind": { + "enum": ["review", "compact"], + "type": "string" + }, + "PatchApplyStatus": { + "enum": ["inProgress", "completed", "failed", "declined"], + "type": "string" + }, + "PatchChangeKind": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["add"], + "title": "AddPatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "AddPatchChangeKind", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["delete"], + "title": "DeletePatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "DeletePatchChangeKind", + "type": "object" + }, + { + "properties": { + "move_path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["update"], + "title": "UpdatePatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "UpdatePatchChangeKind", + "type": "object" + } + ] + }, + "Personality": { + "enum": ["none", "friendly", "pragmatic"], + "type": "string" + }, + "PlanDeltaNotification": { + "description": "EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should not assume concatenated deltas match the completed plan item content.", + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["delta", "itemId", "threadId", "turnId"], + "type": "object" + }, + "PlanType": { + "enum": [ + "free", + "go", + "plus", + "pro", + "prolite", + "team", + "self_serve_business_prolite", + "self_serve_business_usage_based", + "business", + "ent26", + "enterprise_cbp_automation", + "enterprise_cbp_usage_based", + "enterprise", + "edu", + "edu_plus", + "edu_pro", + "unknown" + ], + "type": "string" + }, + "ProcessExitedNotification": { + "description": "Final process exit notification for `process/spawn`.", + "properties": { + "exitCode": { + "description": "Process exit code.", + "format": "int32", + "type": "integer" + }, + "processHandle": { + "description": "Client-supplied, connection-scoped `processHandle` from `process/spawn`.", + "type": "string" + }, + "stderr": { + "description": "Buffered stderr capture.\n\nEmpty when stderr was streamed via `process/outputDelta`.", + "type": "string" + }, + "stderrCapReached": { + "description": "Whether stderr reached `outputBytesCap`.\n\nIn streaming mode, stderr is empty and cap state is also reported on the final stderr `process/outputDelta` notification.", + "type": "boolean" + }, + "stdout": { + "description": "Buffered stdout capture.\n\nEmpty when stdout was streamed via `process/outputDelta`.", + "type": "string" + }, + "stdoutCapReached": { + "description": "Whether stdout reached `outputBytesCap`.\n\nIn streaming mode, stdout is empty and cap state is also reported on the final stdout `process/outputDelta` notification.", + "type": "boolean" + } + }, + "required": [ + "exitCode", + "processHandle", + "stderr", + "stderrCapReached", + "stdout", + "stdoutCapReached" + ], + "type": "object" + }, + "ProcessOutputDeltaNotification": { + "description": "Base64-encoded output chunk emitted for a streaming `process/spawn` request.", + "properties": { + "capReached": { + "description": "True on the final streamed chunk for this stream when output was truncated by `outputBytesCap`.", + "type": "boolean" + }, + "deltaBase64": { + "description": "Base64-encoded output bytes.", + "type": "string" + }, + "processHandle": { + "description": "Client-supplied, connection-scoped `processHandle` from `process/spawn`.", + "type": "string" + }, + "stream": { + "allOf": [ + { + "$ref": "#/definitions/ProcessOutputStream" + } + ], + "description": "Output stream this chunk belongs to." + } + }, + "required": ["capReached", "deltaBase64", "processHandle", "stream"], + "type": "object" + }, + "ProcessOutputStream": { + "description": "Stream label for `process/outputDelta` notifications.", + "oneOf": [ + { + "description": "stdout stream. PTY mode multiplexes terminal output here.", + "enum": ["stdout"], + "type": "string" + }, + { + "description": "stderr stream.", + "enum": ["stderr"], + "type": "string" + } + ] + }, + "ProjectChangeType": { + "enum": ["created", "updated", "deleted"], + "type": "string" + }, + "ProjectChangedNotification": { + "properties": { + "changeType": { + "$ref": "#/definitions/ProjectChangeType" + }, + "projectId": { + "type": "string" + } + }, + "required": ["changeType", "projectId"], + "type": "object" + }, + "RateLimitReachedType": { + "enum": [ + "rate_limit_reached", + "workspace_owner_credits_depleted", + "workspace_member_credits_depleted", + "workspace_owner_usage_limit_reached", + "workspace_member_usage_limit_reached" + ], + "type": "string" + }, + "RateLimitSnapshot": { + "properties": { + "credits": { + "anyOf": [ + { + "$ref": "#/definitions/CreditsSnapshot" + }, + { + "type": "null" + } + ] + }, + "individualLimit": { + "anyOf": [ + { + "$ref": "#/definitions/SpendControlLimitSnapshot" + }, + { + "type": "null" + } + ] + }, + "limitId": { + "type": ["string", "null"] + }, + "limitName": { + "type": ["string", "null"] + }, + "planType": { + "anyOf": [ + { + "$ref": "#/definitions/PlanType" + }, + { + "type": "null" + } + ] + }, + "primary": { + "anyOf": [ + { + "$ref": "#/definitions/RateLimitWindow" + }, + { + "type": "null" + } + ] + }, + "rateLimitReachedType": { + "anyOf": [ + { + "$ref": "#/definitions/RateLimitReachedType" + }, + { + "type": "null" + } + ] + }, + "secondary": { + "anyOf": [ + { + "$ref": "#/definitions/RateLimitWindow" + }, + { + "type": "null" + } + ] + }, + "spendControlReached": { + "description": "Backend-reported spend-control state. `None` is unavailable, not a sparse-update recovery.", + "type": ["boolean", "null"] + } + }, + "type": "object" + }, + "RateLimitWindow": { + "properties": { + "resetsAt": { + "format": "int64", + "type": ["integer", "null"] + }, + "usedPercent": { + "format": "int32", + "type": "integer" + }, + "windowDurationMins": { + "format": "int64", + "type": ["integer", "null"] + } + }, + "required": ["usedPercent"], + "type": "object" + }, + "RealtimeConversationVersion": { + "enum": ["v1", "v2", "v3"], + "type": "string" + }, + "ReasoningEffort": { + "description": "A non-empty reasoning effort value advertised by the model.", + "minLength": 1, + "type": "string" + }, + "ReasoningSummary": { + "description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries", + "oneOf": [ + { + "enum": ["auto", "concise", "detailed"], + "type": "string" + }, + { + "description": "Option to disable reasoning summaries.", + "enum": ["none"], + "type": "string" + } + ] + }, + "ReasoningSummaryPartAddedNotification": { + "properties": { + "itemId": { + "type": "string" + }, + "summaryIndex": { + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["itemId", "summaryIndex", "threadId", "turnId"], + "type": "object" + }, + "ReasoningSummaryTextDeltaNotification": { + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "summaryIndex": { + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["delta", "itemId", "summaryIndex", "threadId", "turnId"], + "type": "object" + }, + "ReasoningTextDeltaNotification": { + "properties": { + "contentIndex": { + "format": "int64", + "type": "integer" + }, + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["contentIndex", "delta", "itemId", "threadId", "turnId"], + "type": "object" + }, + "RemoteControlConnectionStatus": { + "enum": ["disabled", "connecting", "connected", "errored"], + "type": "string" + }, + "RemoteControlStatusChangedNotification": { + "description": "Current remote-control connection status and remote identity exposed to clients.", + "properties": { + "environmentId": { + "type": ["string", "null"] + }, + "installationId": { + "type": "string" + }, + "serverName": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/RemoteControlConnectionStatus" + } + }, + "required": ["installationId", "serverName", "status"], + "type": "object" + }, + "RequestId": { + "anyOf": [ + { + "type": "string" + }, + { + "format": "int64", + "type": "integer" + } + ] + }, + "RequestPermissionProfile": { + "additionalProperties": false, + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "SandboxPolicy": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["dangerFullAccess"], + "title": "DangerFullAccessSandboxPolicyType", + "type": "string" + } + }, + "required": ["type"], + "title": "DangerFullAccessSandboxPolicy", + "type": "object" + }, + { + "properties": { + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "enum": ["readOnly"], + "title": "ReadOnlySandboxPolicyType", + "type": "string" + } + }, + "required": ["type"], + "title": "ReadOnlySandboxPolicy", + "type": "object" + }, + { + "properties": { + "networkAccess": { + "allOf": [ + { + "$ref": "#/definitions/NetworkAccess" + } + ], + "default": "restricted" + }, + "type": { + "enum": ["externalSandbox"], + "title": "ExternalSandboxSandboxPolicyType", + "type": "string" + } + }, + "required": ["type"], + "title": "ExternalSandboxSandboxPolicy", + "type": "object" + }, + { + "properties": { + "excludeSlashTmp": { + "default": false, + "type": "boolean" + }, + "excludeTmpdirEnvVar": { + "default": false, + "type": "boolean" + }, + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "enum": ["workspaceWrite"], + "title": "WorkspaceWriteSandboxPolicyType", + "type": "string" + }, + "writableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": ["type"], + "title": "WorkspaceWriteSandboxPolicy", + "type": "object" + } + ] + }, + "ServerRequestResolvedNotification": { + "properties": { + "requestId": { + "$ref": "#/definitions/RequestId" + }, + "threadId": { + "type": "string" + } + }, + "required": ["requestId", "threadId"], + "type": "object" + }, + "SessionSource": { + "oneOf": [ + { + "enum": ["cli", "vscode", "exec", "appServer", "unknown"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "custom": { + "type": "string" + } + }, + "required": ["custom"], + "title": "CustomSessionSource", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "subAgent": { + "$ref": "#/definitions/SubAgentSource" + } + }, + "required": ["subAgent"], + "title": "SubAgentSessionSource", + "type": "object" + } + ] + }, + "Settings": { + "description": "Settings for a collaboration mode.", + "properties": { + "developer_instructions": { + "type": ["string", "null"] + }, + "model": { + "type": "string" + }, + "reasoning_effort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + } + }, + "required": ["model"], + "type": "object" + }, + "SkillsChangedNotification": { + "description": "Notification emitted when watched local skill files change.\n\nTreat this as an invalidation signal and re-run `skills/list` with the client's current parameters when refreshed skill metadata is needed.", + "type": "object" + }, + "SpendControlLimitSnapshot": { + "properties": { + "limit": { + "type": "string" + }, + "remainingPercent": { + "format": "int32", + "type": "integer" + }, + "resetsAt": { + "format": "int64", + "type": "integer" + }, + "used": { + "type": "string" + } + }, + "required": ["limit", "remainingPercent", "resetsAt", "used"], + "type": "object" + }, + "StrictReviewRequiredNotification": { + "properties": { + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this review started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["startedAtMs", "threadId", "turnId"], + "type": "object" + }, + "SubAgentActivityKind": { + "enum": ["started", "interacted", "interrupted", "completed"], + "type": "string" + }, + "SubAgentSource": { + "oneOf": [ + { + "enum": ["review", "compact", "memory_consolidation"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "thread_spawn": { + "properties": { + "agent_nickname": { + "default": null, + "type": ["string", "null"] + }, + "agent_path": { + "anyOf": [ + { + "$ref": "#/definitions/AgentPath" + }, + { + "type": "null" + } + ], + "default": null + }, + "agent_role": { + "default": null, + "type": ["string", "null"] + }, + "depth": { + "format": "int32", + "type": "integer" + }, + "parent_thread_id": { + "$ref": "#/definitions/ThreadId" + } + }, + "required": ["depth", "parent_thread_id"], + "type": "object" + } + }, + "required": ["thread_spawn"], + "title": "ThreadSpawnSubAgentSource", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "other": { + "type": "string" + } + }, + "required": ["other"], + "title": "OtherSubAgentSource", + "type": "object" + } + ] + }, + "TerminalInteractionNotification": { + "properties": { + "itemId": { + "type": "string" + }, + "processId": { + "type": "string" + }, + "stdin": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["itemId", "processId", "stdin", "threadId", "turnId"], + "type": "object" + }, + "TextElement": { + "properties": { + "byteRange": { + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ], + "description": "Byte range in the parent `text` buffer that this element occupies." + }, + "placeholder": { + "description": "Optional human-readable placeholder for the element, displayed in the UI.", + "type": ["string", "null"] + } + }, + "required": ["byteRange"], + "type": "object" + }, + "TextPosition": { + "properties": { + "column": { + "description": "1-based column number (in Unicode scalar values).", + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "line": { + "description": "1-based line number.", + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": ["column", "line"], + "type": "object" + }, + "TextRange": { + "properties": { + "end": { + "$ref": "#/definitions/TextPosition" + }, + "start": { + "$ref": "#/definitions/TextPosition" + } + }, + "required": ["end", "start"], + "type": "object" + }, + "Thread": { + "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": ["string", "null"] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": ["string", "null"] + }, + "canAcceptDirectInput": { + "description": "Whether the app server accepts direct turn input for this loaded thread. `None` means the capability is unavailable, such as for an unloaded stored thread.", + "type": ["boolean", "null"] + }, + "cliVersion": { + "description": "Version of the CLI that created the thread.", + "type": "string" + }, + "createdAt": { + "description": "Unix timestamp (in seconds) when the thread was created.", + "format": "int64", + "type": "integer" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." + }, + "ephemeral": { + "description": "Whether the thread is ephemeral and should not be materialized on disk.", + "type": "boolean" + }, + "extra": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadExtra" + }, + { + "type": "null" + } + ], + "description": "Optional implementation-specific thread data." + }, + "forkedFromId": { + "description": "Source thread id when this thread was created by forking another thread.", + "type": ["string", "null"] + }, + "gitInfo": { + "anyOf": [ + { + "$ref": "#/definitions/GitInfo" + }, + { + "type": "null" + } + ], + "description": "Optional Git metadata captured when the thread was created." + }, + "historyMode": { + "allOf": [ + { + "$ref": "#/definitions/ThreadHistoryMode" + } + ], + "default": "legacy", + "description": "Persisted thread history contract selected when this thread was created." + }, + "id": { + "description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + "type": "string" + }, + "modelProvider": { + "description": "Model provider used for this thread (for example, 'openai').", + "type": "string" + }, + "name": { + "description": "Optional user-facing thread title.", + "type": ["string", "null"] + }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": ["string", "null"] + }, + "path": { + "description": "[UNSTABLE] Path to the thread on disk.", + "type": ["string", "null"] + }, + "preview": { + "description": "Usually the first user message in the thread, if available.", + "type": "string" + }, + "projectId": { + "description": "Canonical project assignment owned by app-server, if any.", + "type": ["string", "null"] + }, + "recencyAt": { + "description": "Unix timestamp (in seconds) used for thread recency ordering.", + "format": "int64", + "type": ["integer", "null"] + }, + "section": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSection" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The independently persisted section selected for this thread, if any." + }, + "sectionEnteredAt": { + "default": null, + "description": "Unix timestamp in seconds when the thread entered its current section.", + "format": "int64", + "type": ["integer", "null"] + }, + "sessionId": { + "description": "Session id shared by threads that belong to the same session tree.", + "type": "string" + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/SessionSource" + } + ], + "description": "Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.)." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/ThreadStatus" + } + ], + "description": "Current runtime status for the thread." + }, + "threadSource": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ], + "description": "Optional analytics source classification for this thread." + }, + "turns": { + "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "items": { + "$ref": "#/definitions/Turn" + }, + "type": "array" + }, + "updatedAt": { + "description": "Unix timestamp (in seconds) when the thread was last updated.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "cliVersion", + "createdAt", + "cwd", + "ephemeral", + "id", + "modelProvider", + "preview", + "projectId", + "sessionId", + "source", + "status", + "turns", + "updatedAt" + ], + "type": "object" + }, + "ThreadActiveFlag": { + "enum": ["waitingOnApproval", "waitingOnUserInput"], + "type": "string" + }, + "ThreadArchivedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": ["threadId"], + "type": "object" + }, + "ThreadClosedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": ["threadId"], + "type": "object" + }, + "ThreadDeletedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": ["threadId"], + "type": "object" + }, + "ThreadExtra": { + "description": "Extra app-server data for a thread.", + "type": "object" + }, + "ThreadGoal": { + "properties": { + "createdAt": { + "format": "int64", + "type": "integer" + }, + "objective": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/ThreadGoalStatus" + }, + "threadId": { + "type": "string" + }, + "timeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "tokenBudget": { + "format": "int64", + "type": ["integer", "null"] + }, + "tokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAt", + "objective", + "status", + "threadId", + "timeUsedSeconds", + "tokensUsed", + "updatedAt" + ], + "type": "object" + }, + "ThreadGoalClearedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": ["threadId"], + "type": "object" + }, + "ThreadGoalStatus": { + "enum": ["active", "paused", "blocked", "usageLimited", "budgetLimited", "complete"], + "type": "string" + }, + "ThreadGoalUpdatedNotification": { + "properties": { + "goal": { + "$ref": "#/definitions/ThreadGoal" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": ["string", "null"] + } + }, + "required": ["goal", "threadId"], + "type": "object" + }, + "ThreadHistoryMode": { + "enum": ["legacy", "paginated"], + "type": "string" + }, + "ThreadId": { + "type": "string" + }, + "ThreadItem": { + "oneOf": [ + { + "properties": { + "clientId": { + "type": ["string", "null"] + }, + "content": { + "items": { + "$ref": "#/definitions/UserInput" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["userMessage"], + "title": "UserMessageThreadItemType", + "type": "string" + } + }, + "required": ["content", "id", "type"], + "title": "UserMessageThreadItem", + "type": "object" + }, + { + "properties": { + "fragments": { + "items": { + "$ref": "#/definitions/HookPromptFragment" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["hookPrompt"], + "title": "HookPromptThreadItemType", + "type": "string" + } + }, + "required": ["fragments", "id", "type"], + "title": "HookPromptThreadItem", + "type": "object" + }, + { + "properties": { + "delivery": { + "anyOf": [ + { + "$ref": "#/definitions/AgentMessageDelivery" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ], + "default": null + }, + "text": { + "type": "string" + }, + "type": { + "enum": ["agentMessage"], + "title": "AgentMessageThreadItemType", + "type": "string" + } + }, + "required": ["id", "text", "type"], + "title": "AgentMessageThreadItem", + "type": "object" + }, + { + "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "enum": ["plan"], + "title": "PlanThreadItemType", + "type": "string" + } + }, + "required": ["id", "text", "type"], + "title": "PlanThreadItem", + "type": "object" + }, + { + "properties": { + "content": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "enum": ["reasoning"], + "title": "ReasoningThreadItemType", + "type": "string" + } + }, + "required": ["id", "type"], + "title": "ReasoningThreadItem", + "type": "object" + }, + { + "properties": { + "aggregatedOutput": { + "description": "The command's output, aggregated from stdout and stderr.", + "type": ["string", "null"] + }, + "command": { + "description": "The command to be executed.", + "type": "string" + }, + "commandActions": { + "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", + "items": { + "$ref": "#/definitions/CommandAction" + }, + "type": "array" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + } + ], + "description": "The command's working directory." + }, + "durationMs": { + "description": "The duration of the command execution in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "exitCode": { + "description": "The command's exit code.", + "format": "int32", + "type": ["integer", "null"] + }, + "id": { + "type": "string" + }, + "pluginId": { + "default": null, + "description": "Trusted first-party plugin id when this command resolves to one plugin script.", + "type": ["string", "null"] + }, + "processId": { + "description": "Identifier for the underlying PTY process (when available).", + "type": ["string", "null"] + }, + "scriptPath": { + "default": null, + "description": "Safe plugin-relative path when this command resolves to one plugin script.", + "type": ["string", "null"] + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ], + "default": "agent" + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" + }, + "type": { + "enum": ["commandExecution"], + "title": "CommandExecutionThreadItemType", + "type": "string" + } + }, + "required": ["command", "commandActions", "cwd", "id", "status", "type"], + "title": "CommandExecutionThreadItem", + "type": "object" + }, + { + "properties": { + "changes": { + "items": { + "$ref": "#/definitions/FileUpdateChange" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, + "type": { + "enum": ["fileChange"], + "title": "FileChangeThreadItemType", + "type": "string" + } + }, + "required": ["changes", "id", "status", "type"], + "title": "FileChangeThreadItem", + "type": "object" + }, + { + "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, + "arguments": true, + "durationMs": { + "description": "The duration of the MCP tool call in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", + "type": ["string", "null"] + }, + "pluginId": { + "type": ["string", "null"] + }, + "readOnlyHint": { + "type": ["boolean", "null"] + }, + "result": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" + }, + "type": { + "enum": ["mcpToolCall"], + "title": "McpToolCallThreadItemType", + "type": "string" + } + }, + "required": ["arguments", "id", "server", "status", "tool", "type"], + "title": "McpToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": ["array", "null"] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "id": { + "type": "string" + }, + "namespace": { + "type": ["string", "null"] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": ["boolean", "null"] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": ["dynamicToolCall"], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": ["arguments", "id", "status", "tool", "type"], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentsStates": { + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + }, + "description": "Last known status of the target agents, when available.", + "type": "object" + }, + "id": { + "description": "Unique identifier for this collab tool call.", + "type": "string" + }, + "model": { + "description": "Model requested for the spawned agent, when applicable.", + "type": ["string", "null"] + }, + "prompt": { + "description": "Prompt text sent as part of the collab tool call, when available.", + "type": ["string", "null"] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Reasoning effort requested for the spawned agent, when applicable." + }, + "receiverThreadIds": { + "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", + "items": { + "type": "string" + }, + "type": "array" + }, + "senderThreadId": { + "description": "Thread ID of the agent issuing the collab request.", + "type": "string" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ], + "description": "Current status of the collab tool call." + }, + "tool": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ], + "description": "Name of the collab tool that was invoked." + }, + "type": { + "enum": ["collabAgentToolCall"], + "title": "CollabAgentToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "agentsStates", + "id", + "receiverThreadIds", + "senderThreadId", + "status", + "tool", + "type" + ], + "title": "CollabAgentToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentPath": { + "type": "string" + }, + "agentThreadId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/SubAgentActivityKind" + }, + "type": { + "enum": ["subAgentActivity"], + "title": "SubAgentActivityThreadItemType", + "type": "string" + } + }, + "required": ["agentPath", "agentThreadId", "id", "kind", "type"], + "title": "SubAgentActivityThreadItem", + "type": "object" + }, + { + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "results": { + "default": null, + "description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + "items": true, + "type": ["array", "null"] + }, + "type": { + "enum": ["webSearch"], + "title": "WebSearchThreadItemType", + "type": "string" + } + }, + "required": ["id", "query", "type"], + "title": "WebSearchThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["imageView"], + "title": "ImageViewThreadItemType", + "type": "string" + } + }, + "required": ["id", "path", "type"], + "title": "ImageViewThreadItem", + "type": "object" + }, + { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", + "properties": { + "durationMs": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["sleep"], + "title": "SleepThreadItemType", + "type": "string" + } + }, + "required": ["durationMs", "id", "type"], + "title": "SleepThreadItem", + "type": "object" + }, + { + "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": ["string", "null"] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" + }, + "transparentBackground": { + "default": null, + "type": ["boolean", "null"] + }, + "type": { + "enum": ["imageGeneration"], + "title": "ImageGenerationThreadItemType", + "type": "string" + } + }, + "required": ["id", "result", "status", "type"], + "title": "ImageGenerationThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": ["enteredReviewMode"], + "title": "EnteredReviewModeThreadItemType", + "type": "string" + } + }, + "required": ["id", "review", "type"], + "title": "EnteredReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": ["exitedReviewMode"], + "title": "ExitedReviewModeThreadItemType", + "type": "string" + } + }, + "required": ["id", "review", "type"], + "title": "ExitedReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "type": { + "enum": ["contextCompaction"], + "title": "ContextCompactionThreadItemType", + "type": "string" + } + }, + "required": ["id", "type"], + "title": "ContextCompactionThreadItem", + "type": "object" + } + ] + }, + "ThreadNameUpdatedNotification": { + "properties": { + "threadId": { + "type": "string" + }, + "threadName": { + "type": ["string", "null"] + } + }, + "required": ["threadId"], + "type": "object" + }, + "ThreadProjectUpdatedNotification": { + "properties": { + "projectId": { + "type": ["string", "null"] + }, + "threadId": { + "type": "string" + } + }, + "required": ["projectId", "threadId"], + "type": "object" + }, + "ThreadQueueChangedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": ["threadId"], + "type": "object" + }, + "ThreadRealtimeAudioChunk": { + "description": "EXPERIMENTAL - thread realtime audio chunk.", + "properties": { + "data": { + "type": "string" + }, + "itemId": { + "type": ["string", "null"] + }, + "numChannels": { + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "sampleRate": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "samplesPerChannel": { + "format": "uint32", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "required": ["data", "numChannels", "sampleRate"], + "type": "object" + }, + "ThreadRealtimeBemItemPresentation": { + "description": "EXPERIMENTAL - how an existing agent item appears in a realtime conversation.", + "oneOf": [ + { + "properties": { + "type": { + "enum": ["wholeItem"], + "title": "WholeItemThreadRealtimeBemItemPresentationType", + "type": "string" + } + }, + "required": ["type"], + "title": "WholeItemThreadRealtimeBemItemPresentation", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["inlineMarkdown"], + "title": "InlineMarkdownThreadRealtimeBemItemPresentationType", + "type": "string" + } + }, + "required": ["type"], + "title": "InlineMarkdownThreadRealtimeBemItemPresentation", + "type": "object" + }, + { + "properties": { + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "type": { + "enum": ["inlineVisualization"], + "title": "InlineVisualizationThreadRealtimeBemItemPresentationType", + "type": "string" + } + }, + "required": ["index", "type"], + "title": "InlineVisualizationThreadRealtimeBemItemPresentation", + "type": "object" + } + ] + }, + "ThreadRealtimeClosedNotification": { + "description": "EXPERIMENTAL - emitted when thread realtime transport closes.", + "properties": { + "reason": { + "type": ["string", "null"] + }, + "threadId": { + "type": "string" + } + }, + "required": ["threadId"], + "type": "object" + }, + "ThreadRealtimeErrorNotification": { + "description": "EXPERIMENTAL - emitted when thread realtime encounters an error.", + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": ["message", "threadId"], + "type": "object" + }, + "ThreadRealtimeItem": { + "description": "EXPERIMENTAL - a thread-scoped realtime item in the canonical timeline.", + "oneOf": [ + { + "properties": { + "type": { + "enum": ["realtimeSessionStarted"], + "title": "RealtimeSessionStartedThreadRealtimeItemType", + "type": "string" + } + }, + "required": ["type"], + "title": "RealtimeSessionStartedThreadRealtimeItem", + "type": "object" + }, + { + "properties": { + "role": { + "$ref": "#/definitions/ThreadRealtimeTranscriptRole" + }, + "text": { + "type": "string" + }, + "type": { + "enum": ["transcriptSegment"], + "title": "TranscriptSegmentThreadRealtimeItemType", + "type": "string" + } + }, + "required": ["role", "text", "type"], + "title": "TranscriptSegmentThreadRealtimeItem", + "type": "object" + }, + { + "properties": { + "item_id": { + "type": "string" + }, + "presentation": { + "$ref": "#/definitions/ThreadRealtimeBemItemPresentation" + }, + "turn_id": { + "type": "string" + }, + "type": { + "enum": ["bemItemPromoted"], + "title": "BemItemPromotedThreadRealtimeItemType", + "type": "string" + } + }, + "required": ["item_id", "presentation", "turn_id", "type"], + "title": "BemItemPromotedThreadRealtimeItem", + "type": "object" + }, + { + "properties": { + "outcome": { + "$ref": "#/definitions/ThreadRealtimeSessionOutcome" + }, + "type": { + "enum": ["realtimeSessionClosed"], + "title": "RealtimeSessionClosedThreadRealtimeItemType", + "type": "string" + } + }, + "required": ["outcome", "type"], + "title": "RealtimeSessionClosedThreadRealtimeItem", + "type": "object" + } + ], + "properties": { + "id": { + "type": "string" + }, + "realtimeSessionId": { + "type": "string" + } + }, + "required": ["id", "realtimeSessionId"], + "type": "object" + }, + "ThreadRealtimeItemAddedNotification": { + "description": "EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend.", + "properties": { + "item": true, + "threadId": { + "type": "string" + } + }, + "required": ["item", "threadId"], + "type": "object" + }, + "ThreadRealtimeItemCompletedNotification": { + "description": "EXPERIMENTAL - a realtime timeline item published after canonical commit.", + "properties": { + "item": { + "$ref": "#/definitions/ThreadRealtimeItem" + }, + "threadId": { + "type": "string" + } + }, + "required": ["item", "threadId"], + "type": "object" + }, + "ThreadRealtimeItemStartedNotification": { + "description": "EXPERIMENTAL - a realtime timeline item started before its content streams.", + "properties": { + "item": { + "$ref": "#/definitions/ThreadRealtimeItem" + }, + "threadId": { + "type": "string" + } + }, + "required": ["item", "threadId"], + "type": "object" + }, + "ThreadRealtimeItemTranscriptDeltaNotification": { + "description": "EXPERIMENTAL - text appended to an active realtime transcript item.", + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": ["delta", "itemId", "threadId"], + "type": "object" + }, + "ThreadRealtimeOutputAudioDeltaNotification": { + "description": "EXPERIMENTAL - streamed output audio emitted by thread realtime.", + "properties": { + "audio": { + "$ref": "#/definitions/ThreadRealtimeAudioChunk" + }, + "threadId": { + "type": "string" + } + }, + "required": ["audio", "threadId"], + "type": "object" + }, + "ThreadRealtimeSdpNotification": { + "description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.", + "properties": { + "sdp": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": ["sdp", "threadId"], + "type": "object" + }, + "ThreadRealtimeSessionOutcome": { + "enum": ["ended", "failed"], + "type": "string" + }, + "ThreadRealtimeStartedNotification": { + "description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.", + "properties": { + "realtimeSessionId": { + "type": ["string", "null"] + }, + "threadId": { + "type": "string" + }, + "version": { + "$ref": "#/definitions/RealtimeConversationVersion" + } + }, + "required": ["threadId", "version"], + "type": "object" + }, + "ThreadRealtimeTranscriptDeltaNotification": { + "description": "EXPERIMENTAL - flat transcript delta emitted whenever realtime transcript text changes.", + "properties": { + "delta": { + "description": "Live transcript delta from the realtime event.", + "type": "string" + }, + "role": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": ["delta", "role", "threadId"], + "type": "object" + }, + "ThreadRealtimeTranscriptDoneNotification": { + "description": "EXPERIMENTAL - final transcript text emitted when realtime completes a transcript part.", + "properties": { + "role": { + "type": "string" + }, + "text": { + "description": "Final complete text for the transcript part.", + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": ["role", "text", "threadId"], + "type": "object" + }, + "ThreadRealtimeTranscriptRole": { + "enum": ["user", "assistant"], + "type": "string" + }, + "ThreadRevertedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": ["threadId"], + "type": "object" + }, + "ThreadSection": { + "description": "An independently persisted, user-visible thread section.", + "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, + "id": { + "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", + "type": "string" + }, + "name": { + "description": "The current user-visible section name.", + "type": "string" + } + }, + "required": ["id", "name"], + "type": "object" + }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": ["string", "null"] + }, + "icon": { + "type": ["string", "null"] + } + }, + "type": "object" + }, + "ThreadSettings": { + "properties": { + "activePermissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/ActivePermissionProfile" + }, + { + "type": "null" + } + ] + }, + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" + }, + "approvalsReviewer": { + "$ref": "#/definitions/ApprovalsReviewer" + }, + "collaborationMode": { + "$ref": "#/definitions/CollaborationMode" + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "effort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "multiAgentMode": { + "allOf": [ + { + "$ref": "#/definitions/MultiAgentMode" + } + ], + "default": "explicitRequestOnly", + "description": "@deprecated Always `explicitRequestOnly`. Use `effort` for Ultra behavior." + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ] + }, + "sandboxPolicy": { + "$ref": "#/definitions/SandboxPolicy" + }, + "serviceTier": { + "type": ["string", "null"] + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "approvalPolicy", + "approvalsReviewer", + "collaborationMode", + "cwd", + "model", + "modelProvider", + "sandboxPolicy" + ], + "type": "object" + }, + "ThreadSettingsUpdatedNotification": { + "properties": { + "threadId": { + "type": "string" + }, + "threadSettings": { + "$ref": "#/definitions/ThreadSettings" + } + }, + "required": ["threadId", "threadSettings"], + "type": "object" + }, + "ThreadSource": { + "type": "string" + }, + "ThreadStartedNotification": { + "properties": { + "thread": { + "$ref": "#/definitions/Thread" + } + }, + "required": ["thread"], + "type": "object" + }, + "ThreadStatus": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["notLoaded"], + "title": "NotLoadedThreadStatusType", + "type": "string" + } + }, + "required": ["type"], + "title": "NotLoadedThreadStatus", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["idle"], + "title": "IdleThreadStatusType", + "type": "string" + } + }, + "required": ["type"], + "title": "IdleThreadStatus", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["systemError"], + "title": "SystemErrorThreadStatusType", + "type": "string" + } + }, + "required": ["type"], + "title": "SystemErrorThreadStatus", + "type": "object" + }, + { + "properties": { + "activeFlags": { + "items": { + "$ref": "#/definitions/ThreadActiveFlag" + }, + "type": "array" + }, + "type": { + "enum": ["active"], + "title": "ActiveThreadStatusType", + "type": "string" + } + }, + "required": ["activeFlags", "type"], + "title": "ActiveThreadStatus", + "type": "object" + } + ] + }, + "ThreadStatusChangedNotification": { + "properties": { + "status": { + "$ref": "#/definitions/ThreadStatus" + }, + "threadId": { + "type": "string" + } + }, + "required": ["status", "threadId"], + "type": "object" + }, + "ThreadTokenUsage": { + "properties": { + "last": { + "$ref": "#/definitions/TokenUsageBreakdown" + }, + "modelContextWindow": { + "format": "int64", + "type": ["integer", "null"] + }, + "total": { + "$ref": "#/definitions/TokenUsageBreakdown" + } + }, + "required": ["last", "total"], + "type": "object" + }, + "ThreadTokenUsageUpdatedNotification": { + "properties": { + "threadId": { + "type": "string" + }, + "tokenUsage": { + "$ref": "#/definitions/ThreadTokenUsage" + }, + "turnId": { + "type": "string" + } + }, + "required": ["threadId", "tokenUsage", "turnId"], + "type": "object" + }, + "ThreadUnarchivedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": ["threadId"], + "type": "object" + }, + "TokenUsageBreakdown": { + "properties": { + "cacheWriteInputTokens": { + "default": 0, + "format": "int64", + "type": "integer" + }, + "cachedInputTokens": { + "format": "int64", + "type": "integer" + }, + "inputTokens": { + "format": "int64", + "type": "integer" + }, + "outputTokens": { + "format": "int64", + "type": "integer" + }, + "reasoningOutputTokens": { + "format": "int64", + "type": "integer" + }, + "totalTokens": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "cachedInputTokens", + "inputTokens", + "outputTokens", + "reasoningOutputTokens", + "totalTokens" + ], + "type": "object" + }, + "Turn": { + "properties": { + "completedAt": { + "description": "Unix timestamp (in seconds) when the turn completed.", + "format": "int64", + "type": ["integer", "null"] + }, + "durationMs": { + "description": "Duration between turn start and completion in milliseconds, if known.", + "format": "int64", + "type": ["integer", "null"] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ], + "description": "Only populated when the Turn's status is failed." + }, + "id": { + "description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + "type": "string" + }, + "items": { + "description": "Thread items currently included in this turn payload.", + "items": { + "$ref": "#/definitions/ThreadItem" + }, + "type": "array" + }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, + "startedAt": { + "description": "Unix timestamp (in seconds) when the turn started.", + "format": "int64", + "type": ["integer", "null"] + }, + "status": { + "$ref": "#/definitions/TurnStatus" + } + }, + "required": ["id", "items", "status"], + "type": "object" + }, + "TurnCompletedNotification": { + "properties": { + "threadId": { + "type": "string" + }, + "turn": { + "$ref": "#/definitions/Turn" + } + }, + "required": ["threadId", "turn"], + "type": "object" + }, + "TurnDiffUpdatedNotification": { + "description": "Notification that the turn-level unified diff has changed. Contains the latest aggregated diff across all file changes in the turn.", + "properties": { + "diff": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["diff", "threadId", "turnId"], + "type": "object" + }, + "TurnError": { + "properties": { + "additionalDetails": { + "default": null, + "type": ["string", "null"] + }, + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": ["notLoaded"], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": ["summary"], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": ["full"], + "type": "string" + } + ] + }, + "TurnModerationMetadataNotification": { + "properties": { + "metadata": true, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["metadata", "threadId", "turnId"], + "type": "object" + }, + "TurnPlanStep": { + "properties": { + "status": { + "$ref": "#/definitions/TurnPlanStepStatus" + }, + "step": { + "type": "string" + } + }, + "required": ["status", "step"], + "type": "object" + }, + "TurnPlanStepStatus": { + "enum": ["pending", "inProgress", "completed"], + "type": "string" + }, + "TurnPlanUpdatedNotification": { + "properties": { + "explanation": { + "type": ["string", "null"] + }, + "plan": { + "items": { + "$ref": "#/definitions/TurnPlanStep" + }, + "type": "array" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["plan", "threadId", "turnId"], + "type": "object" + }, + "TurnStartedNotification": { + "properties": { + "threadId": { + "type": "string" + }, + "turn": { + "$ref": "#/definitions/Turn" + } + }, + "required": ["threadId", "turn"], + "type": "object" + }, + "TurnStatus": { + "enum": ["completed", "interrupted", "failed", "inProgress"], + "type": "string" + }, + "UserInput": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "text_elements": { + "default": [], + "description": "UI-defined spans within `text` used to render or persist special elements.", + "items": { + "$ref": "#/definitions/TextElement" + }, + "type": "array" + }, + "type": { + "enum": ["text"], + "title": "TextUserInputType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "TextUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "type": { + "enum": ["image"], + "title": "ImageUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["type", "url"], + "title": "ImageUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["localImage"], + "title": "LocalImageUserInputType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "LocalImageUserInput", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["audio"], + "title": "AudioUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["type", "url"], + "title": "AudioUserInput", + "type": "object" + }, + { + "properties": { + "path": { + "type": "string" + }, + "type": { + "enum": ["localAudio"], + "title": "LocalAudioUserInputType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "LocalAudioUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["skill"], + "title": "SkillUserInputType", + "type": "string" + } + }, + "required": ["name", "path", "type"], + "title": "SkillUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["mention"], + "title": "MentionUserInputType", + "type": "string" + } + }, + "required": ["name", "path", "type"], + "title": "MentionUserInput", + "type": "object" + } + ] + }, + "WarningNotification": { + "properties": { + "message": { + "description": "Concise warning message for the user.", + "type": "string" + }, + "threadId": { + "description": "Optional thread target when the warning applies to a specific thread.", + "type": ["string", "null"] + } + }, + "required": ["message"], + "type": "object" + }, + "WebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchWebSearchActionType", + "type": "string" + } + }, + "required": ["type"], + "title": "SearchWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["openPage"], + "title": "OpenPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": ["string", "null"] + } + }, + "required": ["type"], + "title": "OpenPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": ["string", "null"] + }, + "type": { + "enum": ["findInPage"], + "title": "FindInPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": ["string", "null"] + } + }, + "required": ["type"], + "title": "FindInPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["other"], + "title": "OtherWebSearchActionType", + "type": "string" + } + }, + "required": ["type"], + "title": "OtherWebSearchAction", + "type": "object" + } + ] + }, + "WindowsSandboxSetupCompletedNotification": { + "properties": { + "error": { + "type": ["string", "null"] + }, + "mode": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + }, + "success": { + "type": "boolean" + } + }, + "required": ["mode", "success"], + "type": "object" + }, + "WindowsSandboxSetupMode": { + "enum": ["elevated", "unelevated"], + "type": "string" + }, + "WindowsWorldWritableWarningNotification": { + "properties": { + "extraCount": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "failedScan": { + "type": "boolean" + }, + "samplePaths": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["extraCount", "failedScan", "samplePaths"], + "type": "object" + } + }, + "description": "Notification sent from the server to the client.", + "oneOf": [ + { + "description": "NEW NOTIFICATIONS", + "properties": { + "method": { + "enum": ["error"], + "title": "ErrorNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ErrorNotification" + } + }, + "required": ["method", "params"], + "title": "ErrorNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/started"], + "title": "Thread/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadStartedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/status/changed"], + "title": "Thread/status/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadStatusChangedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/status/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/archived"], + "title": "Thread/archivedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadArchivedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/archivedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/deleted"], + "title": "Thread/deletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadDeletedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/deletedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/unarchived"], + "title": "Thread/unarchivedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUnarchivedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/unarchivedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/closed"], + "title": "Thread/closedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadClosedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/closedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/reverted"], + "title": "Thread/revertedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRevertedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/revertedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["skills/changed"], + "title": "Skills/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsChangedNotification" + } + }, + "required": ["method", "params"], + "title": "Skills/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/name/updated"], + "title": "Thread/name/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadNameUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/name/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/goal/updated"], + "title": "Thread/goal/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/goal/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/goal/cleared"], + "title": "Thread/goal/clearedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalClearedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/goal/clearedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/queue/changed"], + "title": "Thread/queue/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadQueueChangedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/queue/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["project/changed"], + "title": "Project/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ProjectChangedNotification" + } + }, + "required": ["method", "params"], + "title": "Project/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/project/updated"], + "title": "Thread/project/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadProjectUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/project/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/environment/connected"], + "title": "Thread/environment/connectedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/EnvironmentConnectionNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/environment/connectedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/environment/disconnected"], + "title": "Thread/environment/disconnectedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/EnvironmentConnectionNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/environment/disconnectedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/settings/updated"], + "title": "Thread/settings/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadSettingsUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/settings/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/tokenUsage/updated"], + "title": "Thread/tokenUsage/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadTokenUsageUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/tokenUsage/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["turn/started"], + "title": "Turn/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnStartedNotification" + } + }, + "required": ["method", "params"], + "title": "Turn/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["hook/started"], + "title": "Hook/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/HookStartedNotification" + } + }, + "required": ["method", "params"], + "title": "Hook/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["turn/completed"], + "title": "Turn/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "Turn/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["hook/completed"], + "title": "Hook/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/HookCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "Hook/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["turn/diff/updated"], + "title": "Turn/diff/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnDiffUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Turn/diff/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["turn/plan/updated"], + "title": "Turn/plan/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnPlanUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Turn/plan/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/started"], + "title": "Item/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ItemStartedNotification" + } + }, + "required": ["method", "params"], + "title": "Item/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/autoApprovalReview/started"], + "title": "Item/autoApprovalReview/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ItemGuardianApprovalReviewStartedNotification" + } + }, + "required": ["method", "params"], + "title": "Item/autoApprovalReview/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/autoApprovalReview/completed"], + "title": "Item/autoApprovalReview/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ItemGuardianApprovalReviewCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "Item/autoApprovalReview/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["autoApprovalReview/strictReviewRequired"], + "title": "AutoApprovalReview/strictReviewRequiredNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/StrictReviewRequiredNotification" + } + }, + "required": ["method", "params"], + "title": "AutoApprovalReview/strictReviewRequiredNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/completed"], + "title": "Item/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ItemCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "Item/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/agentMessage/delta"], + "title": "Item/agentMessage/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AgentMessageDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Item/agentMessage/deltaNotification", + "type": "object" + }, + { + "description": "EXPERIMENTAL - proposed plan streaming deltas for plan items.", + "properties": { + "method": { + "enum": ["item/plan/delta"], + "title": "Item/plan/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PlanDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Item/plan/deltaNotification", + "type": "object" + }, + { + "description": "Stream base64-encoded stdout/stderr chunks for a running `command/exec` session.", + "properties": { + "method": { + "enum": ["command/exec/outputDelta"], + "title": "Command/exec/outputDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecOutputDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Command/exec/outputDeltaNotification", + "type": "object" + }, + { + "description": "Stream base64-encoded stdout/stderr chunks for a running `process/spawn` session.", + "properties": { + "method": { + "enum": ["process/outputDelta"], + "title": "Process/outputDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ProcessOutputDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Process/outputDeltaNotification", + "type": "object" + }, + { + "description": "Final exit notification for a `process/spawn` session.", + "properties": { + "method": { + "enum": ["process/exited"], + "title": "Process/exitedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ProcessExitedNotification" + } + }, + "required": ["method", "params"], + "title": "Process/exitedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/commandExecution/outputDelta"], + "title": "Item/commandExecution/outputDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecutionOutputDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Item/commandExecution/outputDeltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/commandExecution/terminalInteraction"], + "title": "Item/commandExecution/terminalInteractionNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TerminalInteractionNotification" + } + }, + "required": ["method", "params"], + "title": "Item/commandExecution/terminalInteractionNotification", + "type": "object" + }, + { + "description": "Deprecated legacy apply_patch output stream notification.", + "properties": { + "method": { + "enum": ["item/fileChange/outputDelta"], + "title": "Item/fileChange/outputDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FileChangeOutputDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Item/fileChange/outputDeltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/fileChange/patchUpdated"], + "title": "Item/fileChange/patchUpdatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FileChangePatchUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Item/fileChange/patchUpdatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["serverRequest/resolved"], + "title": "ServerRequest/resolvedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ServerRequestResolvedNotification" + } + }, + "required": ["method", "params"], + "title": "ServerRequest/resolvedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/mcpToolCall/progress"], + "title": "Item/mcpToolCall/progressNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpToolCallProgressNotification" + } + }, + "required": ["method", "params"], + "title": "Item/mcpToolCall/progressNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["mcpServer/oauthLogin/completed"], + "title": "McpServer/oauthLogin/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerOauthLoginCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "McpServer/oauthLogin/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["mcpServer/startupStatus/updated"], + "title": "McpServer/startupStatus/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerStatusUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "McpServer/startupStatus/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["mcpServer/event/stream/notification"], + "title": "McpServer/event/stream/notificationNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerEventStreamNotification" + } + }, + "required": ["method", "params"], + "title": "McpServer/event/stream/notificationNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["account/updated"], + "title": "Account/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AccountUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Account/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["account/rateLimits/updated"], + "title": "Account/rateLimits/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AccountRateLimitsUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Account/rateLimits/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["app/list/updated"], + "title": "App/list/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AppListUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "App/list/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["remoteControl/status/changed"], + "title": "RemoteControl/status/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/RemoteControlStatusChangedNotification" + } + }, + "required": ["method", "params"], + "title": "RemoteControl/status/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["externalAgentConfig/import/progress"], + "title": "ExternalAgentConfig/import/progressNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigImportProgressNotification" + } + }, + "required": ["method", "params"], + "title": "ExternalAgentConfig/import/progressNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["externalAgentConfig/import/completed"], + "title": "ExternalAgentConfig/import/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigImportCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "ExternalAgentConfig/import/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["fs/changed"], + "title": "Fs/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsChangedNotification" + } + }, + "required": ["method", "params"], + "title": "Fs/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/reasoning/summaryTextDelta"], + "title": "Item/reasoning/summaryTextDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReasoningSummaryTextDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Item/reasoning/summaryTextDeltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/reasoning/summaryPartAdded"], + "title": "Item/reasoning/summaryPartAddedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReasoningSummaryPartAddedNotification" + } + }, + "required": ["method", "params"], + "title": "Item/reasoning/summaryPartAddedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["item/reasoning/textDelta"], + "title": "Item/reasoning/textDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReasoningTextDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Item/reasoning/textDeltaNotification", + "type": "object" + }, + { + "description": "Deprecated: Use `ContextCompaction` item type instead.", + "properties": { + "method": { + "enum": ["thread/compacted"], + "title": "Thread/compactedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ContextCompactedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/compactedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["model/rerouted"], + "title": "Model/reroutedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ModelReroutedNotification" + } + }, + "required": ["method", "params"], + "title": "Model/reroutedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["model/verification"], + "title": "Model/verificationNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ModelVerificationNotification" + } + }, + "required": ["method", "params"], + "title": "Model/verificationNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["turn/moderationMetadata"], + "title": "Turn/moderationMetadataNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnModerationMetadataNotification" + } + }, + "required": ["method", "params"], + "title": "Turn/moderationMetadataNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["model/safetyBuffering/updated"], + "title": "Model/safetyBuffering/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ModelSafetyBufferingUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "Model/safetyBuffering/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["warning"], + "title": "WarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WarningNotification" + } + }, + "required": ["method", "params"], + "title": "WarningNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["guardianWarning"], + "title": "GuardianWarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/GuardianWarningNotification" + } + }, + "required": ["method", "params"], + "title": "GuardianWarningNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["deprecationNotice"], + "title": "DeprecationNoticeNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/DeprecationNoticeNotification" + } + }, + "required": ["method", "params"], + "title": "DeprecationNoticeNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["configWarning"], + "title": "ConfigWarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ConfigWarningNotification" + } + }, + "required": ["method", "params"], + "title": "ConfigWarningNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["fuzzyFileSearch/sessionUpdated"], + "title": "FuzzyFileSearch/sessionUpdatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FuzzyFileSearchSessionUpdatedNotification" + } + }, + "required": ["method", "params"], + "title": "FuzzyFileSearch/sessionUpdatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["fuzzyFileSearch/sessionCompleted"], + "title": "FuzzyFileSearch/sessionCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FuzzyFileSearchSessionCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "FuzzyFileSearch/sessionCompletedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/started"], + "title": "Thread/realtime/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeStartedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/itemAdded"], + "title": "Thread/realtime/itemAddedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeItemAddedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/itemAddedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/item/started"], + "title": "Thread/realtime/item/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeItemStartedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/item/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/item/transcript/delta"], + "title": "Thread/realtime/item/transcript/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeItemTranscriptDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/item/transcript/deltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/item/completed"], + "title": "Thread/realtime/item/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeItemCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/item/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/transcript/delta"], + "title": "Thread/realtime/transcript/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeTranscriptDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/transcript/deltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/transcript/done"], + "title": "Thread/realtime/transcript/doneNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeTranscriptDoneNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/transcript/doneNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/outputAudio/delta"], + "title": "Thread/realtime/outputAudio/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeOutputAudioDeltaNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/outputAudio/deltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/sdp"], + "title": "Thread/realtime/sdpNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeSdpNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/sdpNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/error"], + "title": "Thread/realtime/errorNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeErrorNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/errorNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["thread/realtime/closed"], + "title": "Thread/realtime/closedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeClosedNotification" + } + }, + "required": ["method", "params"], + "title": "Thread/realtime/closedNotification", + "type": "object" + }, + { + "description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.", + "properties": { + "method": { + "enum": ["windows/worldWritableWarning"], + "title": "Windows/worldWritableWarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WindowsWorldWritableWarningNotification" + } + }, + "required": ["method", "params"], + "title": "Windows/worldWritableWarningNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["windowsSandbox/setupCompleted"], + "title": "WindowsSandbox/setupCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WindowsSandboxSetupCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "WindowsSandbox/setupCompletedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["account/login/completed"], + "title": "Account/login/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AccountLoginCompletedNotification" + } + }, + "required": ["method", "params"], + "title": "Account/login/completedNotification", + "type": "object" + } + ], + "properties": { + "emittedAtMs": { + "description": "Unix timestamp (in milliseconds) when app-server emitted this notification.", + "format": "int64", + "type": "integer" + } + }, + "title": "ServerNotification" +} diff --git a/src/runtimes/openai/generated-json-schema/ServerRequest.json b/src/runtimes/openai/generated-json-schema/ServerRequest.json new file mode 100644 index 0000000..3ac9133 --- /dev/null +++ b/src/runtimes/openai/generated-json-schema/ServerRequest.json @@ -0,0 +1,1605 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "AdditionalFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": ["array", "null"] + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": ["integer", "null"] + }, + "read": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": ["array", "null"] + }, + "write": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": ["array", "null"] + } + }, + "type": "object" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": ["boolean", "null"] + } + }, + "type": "object" + }, + "AdditionalPermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ], + "description": "Partial overlay used for per-command permission requests." + } + }, + "type": "object" + }, + "ApplyPatchApprovalParams": { + "properties": { + "callId": { + "description": "Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent] and [codex_protocol::protocol::PatchApplyEndEvent].", + "type": "string" + }, + "conversationId": { + "$ref": "#/definitions/ThreadId" + }, + "fileChanges": { + "additionalProperties": { + "$ref": "#/definitions/FileChange" + }, + "type": "object" + }, + "grantRoot": { + "description": "When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).", + "type": ["string", "null"] + }, + "reason": { + "description": "Optional explanatory reason (e.g. request for extra write access).", + "type": ["string", "null"] + } + }, + "required": ["callId", "conversationId", "fileChanges"], + "type": "object" + }, + "AttestationGenerateParams": { + "type": "object" + }, + "ChatgptAuthTokensRefreshParams": { + "properties": { + "previousAccountId": { + "description": "Workspace/account identifier that Codex was previously using.\n\nClients that manage multiple accounts/workspaces can use this as a hint to refresh the token for the correct workspace.\n\nThis may be `null` when the prior auth state did not include a workspace identifier (`chatgpt_account_id`).", + "type": ["string", "null"] + }, + "reason": { + "$ref": "#/definitions/ChatgptAuthTokensRefreshReason" + } + }, + "required": ["reason"], + "type": "object" + }, + "ChatgptAuthTokensRefreshReason": { + "oneOf": [ + { + "description": "Codex attempted a backend request and received `401 Unauthorized`.", + "enum": ["unauthorized"], + "type": "string" + } + ] + }, + "CommandAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["read"], + "title": "ReadCommandActionType", + "type": "string" + } + }, + "required": ["command", "name", "path", "type"], + "title": "ReadCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["listFiles"], + "title": "ListFilesCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "ListFilesCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "SearchCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "type": { + "enum": ["unknown"], + "title": "UnknownCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "UnknownCommandAction", + "type": "object" + } + ] + }, + "CommandExecutionApprovalDecision": { + "oneOf": [ + { + "description": "User approved the command.", + "enum": ["accept"], + "type": "string" + }, + { + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", + "enum": ["acceptForSession"], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.", + "properties": { + "acceptWithExecpolicyAmendment": { + "properties": { + "execpolicy_amendment": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["execpolicy_amendment"], + "type": "object" + } + }, + "required": ["acceptWithExecpolicyAmendment"], + "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "additionalProperties": false, + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "properties": { + "applyNetworkPolicyAmendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": ["network_policy_amendment"], + "type": "object" + } + }, + "required": ["applyNetworkPolicyAmendment"], + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "description": "User denied the command. The agent will continue the turn.", + "enum": ["decline"], + "type": "string" + }, + { + "description": "User denied the command. The turn will also be immediately interrupted.", + "enum": ["cancel"], + "type": "string" + } + ] + }, + "CommandExecutionApprovalKind": { + "description": "Distinguishes a command approval from input sent to an existing terminal.", + "enum": ["command", "writeStdin"], + "type": "string" + }, + "CommandExecutionRequestApprovalParams": { + "properties": { + "additionalPermissions": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalPermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Optional additional permissions requested for this command." + }, + "approvalId": { + "description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is null.\n\nFor zsh-exec-bridge subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing. Stdin approvals also use a distinct callback id; inspect `kind` to distinguish them.", + "type": ["string", "null"] + }, + "availableDecisions": { + "description": "Ordered list of decisions the client may present for this prompt.", + "items": { + "$ref": "#/definitions/CommandExecutionApprovalDecision" + }, + "type": ["array", "null"] + }, + "command": { + "description": "The command to be executed.", + "type": ["string", "null"] + }, + "commandActions": { + "description": "Best-effort parsed command actions for friendly display.", + "items": { + "$ref": "#/definitions/CommandAction" + }, + "type": ["array", "null"] + }, + "cwd": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ], + "description": "The command's working directory." + }, + "environmentId": { + "default": null, + "description": "Environment in which the command will run.", + "type": ["string", "null"] + }, + "itemId": { + "type": "string" + }, + "kind": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionApprovalKind" + } + ], + "default": "command", + "description": "Kind of action under review. Defaults to `command` for older servers." + }, + "networkApprovalContext": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkApprovalContext" + }, + { + "type": "null" + } + ], + "description": "Optional context for a managed-network approval prompt." + }, + "proposedExecpolicyAmendment": { + "description": "Optional proposed execpolicy amendment to allow similar commands without prompting.", + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "proposedNetworkPolicyAmendments": { + "description": "Optional proposed network policy amendments (allow/deny host) for future requests.", + "items": { + "$ref": "#/definitions/NetworkPolicyAmendment" + }, + "type": ["array", "null"] + }, + "reason": { + "description": "Optional explanatory reason (e.g. request for network access).", + "type": ["string", "null"] + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this approval request started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["itemId", "startedAtMs", "threadId", "turnId"], + "type": "object" + }, + "CurrentTimeReadParams": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": ["threadId"], + "type": "object" + }, + "DynamicToolCallParams": { + "properties": { + "arguments": true, + "callId": { + "type": "string" + }, + "namespace": { + "type": ["string", "null"] + }, + "threadId": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["arguments", "callId", "threadId", "tool", "turnId"], + "type": "object" + }, + "ExecCommandApprovalParams": { + "properties": { + "approvalId": { + "description": "Identifier for this specific approval callback.", + "type": ["string", "null"] + }, + "callId": { + "description": "Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent] and [codex_protocol::protocol::ExecCommandEndEvent].", + "type": "string" + }, + "command": { + "items": { + "type": "string" + }, + "type": "array" + }, + "conversationId": { + "$ref": "#/definitions/ThreadId" + }, + "cwd": { + "type": "string" + }, + "parsedCmd": { + "items": { + "$ref": "#/definitions/ParsedCommand" + }, + "type": "array" + }, + "reason": { + "type": ["string", "null"] + } + }, + "required": ["callId", "command", "conversationId", "cwd", "parsedCmd"], + "type": "object" + }, + "FileChange": { + "oneOf": [ + { + "properties": { + "content": { + "type": "string" + }, + "type": { + "enum": ["add"], + "title": "AddFileChangeType", + "type": "string" + } + }, + "required": ["content", "type"], + "title": "AddFileChange", + "type": "object" + }, + { + "properties": { + "content": { + "type": "string" + }, + "type": { + "enum": ["delete"], + "title": "DeleteFileChangeType", + "type": "string" + } + }, + "required": ["content", "type"], + "title": "DeleteFileChange", + "type": "object" + }, + { + "properties": { + "move_path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["update"], + "title": "UpdateFileChangeType", + "type": "string" + }, + "unified_diff": { + "type": "string" + } + }, + "required": ["type", "unified_diff"], + "title": "UpdateFileChange", + "type": "object" + } + ] + }, + "FileChangeRequestApprovalParams": { + "properties": { + "grantRoot": { + "description": "[UNSTABLE] When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).", + "type": ["string", "null"] + }, + "itemId": { + "type": "string" + }, + "reason": { + "description": "Optional explanatory reason (e.g. request for extra write access).", + "type": ["string", "null"] + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this approval request started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["itemId", "startedAtMs", "threadId", "turnId"], + "type": "object" + }, + "FileSystemAccessMode": { + "enum": ["read", "write", "deny"], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["path"], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": ["glob_pattern"], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": ["pattern", "type"], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["special"], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": ["type", "value"], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": ["access", "path"], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": ["root"], + "type": "string" + } + }, + "required": ["kind"], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["minimal"], + "type": "string" + } + }, + "required": ["kind"], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["project_roots"], + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": ["kind"], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["tmpdir"], + "type": "string" + } + }, + "required": ["kind"], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["slash_tmp"], + "type": "string" + } + }, + "required": ["kind"], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": ["unknown"], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": ["kind", "path"], + "type": "object" + } + ] + }, + "LegacyAppPathString": { + "type": "string" + }, + "McpElicitationArrayType": { + "enum": ["array"], + "type": "string" + }, + "McpElicitationBooleanSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": ["boolean", "null"] + }, + "description": { + "type": ["string", "null"] + }, + "title": { + "type": ["string", "null"] + }, + "type": { + "$ref": "#/definitions/McpElicitationBooleanType" + } + }, + "required": ["type"], + "type": "object" + }, + "McpElicitationBooleanType": { + "enum": ["boolean"], + "type": "string" + }, + "McpElicitationConstOption": { + "additionalProperties": false, + "properties": { + "const": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": ["const", "title"], + "type": "object" + }, + "McpElicitationEnumSchema": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationSingleSelectEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationMultiSelectEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationLegacyTitledEnumSchema" + } + ] + }, + "McpElicitationLegacyTitledEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": ["string", "null"] + }, + "description": { + "type": ["string", "null"] + }, + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "enumNames": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "title": { + "type": ["string", "null"] + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": ["enum", "type"], + "type": "object" + }, + "McpElicitationMultiSelectEnumSchema": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationUntitledMultiSelectEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationTitledMultiSelectEnumSchema" + } + ] + }, + "McpElicitationNumberSchema": { + "additionalProperties": false, + "properties": { + "default": { + "format": "double", + "type": ["number", "null"] + }, + "description": { + "type": ["string", "null"] + }, + "maximum": { + "format": "double", + "type": ["number", "null"] + }, + "minimum": { + "format": "double", + "type": ["number", "null"] + }, + "title": { + "type": ["string", "null"] + }, + "type": { + "$ref": "#/definitions/McpElicitationNumberType" + } + }, + "required": ["type"], + "type": "object" + }, + "McpElicitationNumberType": { + "enum": ["number", "integer"], + "type": "string" + }, + "McpElicitationObjectType": { + "enum": ["object"], + "type": "string" + }, + "McpElicitationPrimitiveSchema": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationStringSchema" + }, + { + "$ref": "#/definitions/McpElicitationNumberSchema" + }, + { + "$ref": "#/definitions/McpElicitationBooleanSchema" + } + ] + }, + "McpElicitationSchema": { + "additionalProperties": false, + "description": "Typed form schema for MCP `elicitation/create` requests.\n\nThis matches the `requestedSchema` shape from the MCP 2025-11-25 `ElicitRequestFormParams` schema.", + "properties": { + "$schema": { + "type": ["string", "null"] + }, + "properties": { + "additionalProperties": { + "$ref": "#/definitions/McpElicitationPrimitiveSchema" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "type": { + "$ref": "#/definitions/McpElicitationObjectType" + } + }, + "required": ["properties", "type"], + "type": "object" + }, + "McpElicitationSingleSelectEnumSchema": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationUntitledSingleSelectEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationTitledSingleSelectEnumSchema" + } + ] + }, + "McpElicitationStringFormat": { + "enum": ["email", "uri", "date", "date-time"], + "type": "string" + }, + "McpElicitationStringSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": ["string", "null"] + }, + "description": { + "type": ["string", "null"] + }, + "format": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationStringFormat" + }, + { + "type": "null" + } + ] + }, + "maxLength": { + "format": "uint32", + "minimum": 0.0, + "type": ["integer", "null"] + }, + "minLength": { + "format": "uint32", + "minimum": 0.0, + "type": ["integer", "null"] + }, + "title": { + "type": ["string", "null"] + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": ["type"], + "type": "object" + }, + "McpElicitationStringType": { + "enum": ["string"], + "type": "string" + }, + "McpElicitationTitledEnumItems": { + "additionalProperties": false, + "properties": { + "anyOf": { + "items": { + "$ref": "#/definitions/McpElicitationConstOption" + }, + "type": "array" + } + }, + "required": ["anyOf"], + "type": "object" + }, + "McpElicitationTitledMultiSelectEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "description": { + "type": ["string", "null"] + }, + "items": { + "$ref": "#/definitions/McpElicitationTitledEnumItems" + }, + "maxItems": { + "format": "uint64", + "minimum": 0.0, + "type": ["integer", "null"] + }, + "minItems": { + "format": "uint64", + "minimum": 0.0, + "type": ["integer", "null"] + }, + "title": { + "type": ["string", "null"] + }, + "type": { + "$ref": "#/definitions/McpElicitationArrayType" + } + }, + "required": ["items", "type"], + "type": "object" + }, + "McpElicitationTitledSingleSelectEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": ["string", "null"] + }, + "description": { + "type": ["string", "null"] + }, + "oneOf": { + "items": { + "$ref": "#/definitions/McpElicitationConstOption" + }, + "type": "array" + }, + "title": { + "type": ["string", "null"] + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": ["oneOf", "type"], + "type": "object" + }, + "McpElicitationUntitledEnumItems": { + "additionalProperties": false, + "properties": { + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": ["enum", "type"], + "type": "object" + }, + "McpElicitationUntitledMultiSelectEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "description": { + "type": ["string", "null"] + }, + "items": { + "$ref": "#/definitions/McpElicitationUntitledEnumItems" + }, + "maxItems": { + "format": "uint64", + "minimum": 0.0, + "type": ["integer", "null"] + }, + "minItems": { + "format": "uint64", + "minimum": 0.0, + "type": ["integer", "null"] + }, + "title": { + "type": ["string", "null"] + }, + "type": { + "$ref": "#/definitions/McpElicitationArrayType" + } + }, + "required": ["items", "type"], + "type": "object" + }, + "McpElicitationUntitledSingleSelectEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": ["string", "null"] + }, + "description": { + "type": ["string", "null"] + }, + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": ["string", "null"] + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": ["enum", "type"], + "type": "object" + }, + "McpServerElicitationRequestParams": { + "oneOf": [ + { + "properties": { + "_meta": true, + "message": { + "type": "string" + }, + "mode": { + "enum": ["form"], + "type": "string" + }, + "requestedSchema": { + "$ref": "#/definitions/McpElicitationSchema" + } + }, + "required": ["message", "mode", "requestedSchema"], + "type": "object" + }, + { + "properties": { + "_meta": true, + "message": { + "type": "string" + }, + "mode": { + "enum": ["openai/form"], + "type": "string" + }, + "requestedSchema": true + }, + "required": ["message", "mode", "requestedSchema"], + "type": "object" + }, + { + "properties": { + "_meta": true, + "elicitationId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "mode": { + "enum": ["url"], + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["elicitationId", "message", "mode", "url"], + "type": "object" + } + ], + "properties": { + "serverName": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "description": "Active Codex turn when this elicitation was observed, if app-server could correlate one.\n\nThis is nullable because MCP models elicitation as a standalone server-to-client request identified by the MCP server request id. It may be triggered during a turn, but turn context is app-server correlation rather than part of the protocol identity of the elicitation itself.", + "type": ["string", "null"] + } + }, + "required": ["serverName", "threadId"], + "type": "object" + }, + "NetworkApprovalContext": { + "properties": { + "host": { + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/NetworkApprovalProtocol" + } + }, + "required": ["host", "protocol"], + "type": "object" + }, + "NetworkApprovalProtocol": { + "enum": ["http", "https", "socks5Tcp", "socks5Udp"], + "type": "string" + }, + "NetworkPolicyAmendment": { + "properties": { + "action": { + "$ref": "#/definitions/NetworkPolicyRuleAction" + }, + "host": { + "type": "string" + } + }, + "required": ["action", "host"], + "type": "object" + }, + "NetworkPolicyRuleAction": { + "enum": ["allow", "deny"], + "type": "string" + }, + "ParsedCommand": { + "oneOf": [ + { + "properties": { + "cmd": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "description": "(Best effort) Path to the file being read by the command. When possible, this is an absolute path, though when relative, it should be resolved against the `cwd`` that will be used to run the command to derive the absolute path.", + "type": "string" + }, + "type": { + "enum": ["read"], + "title": "ReadParsedCommandType", + "type": "string" + } + }, + "required": ["cmd", "name", "path", "type"], + "title": "ReadParsedCommand", + "type": "object" + }, + { + "properties": { + "cmd": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["list_files"], + "title": "ListFilesParsedCommandType", + "type": "string" + } + }, + "required": ["cmd", "type"], + "title": "ListFilesParsedCommand", + "type": "object" + }, + { + "properties": { + "cmd": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchParsedCommandType", + "type": "string" + } + }, + "required": ["cmd", "type"], + "title": "SearchParsedCommand", + "type": "object" + }, + { + "properties": { + "cmd": { + "type": "string" + }, + "type": { + "enum": ["unknown"], + "title": "UnknownParsedCommandType", + "type": "string" + } + }, + "required": ["cmd", "type"], + "title": "UnknownParsedCommand", + "type": "object" + } + ] + }, + "PermissionsRequestApprovalParams": { + "properties": { + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "environmentId": { + "default": null, + "type": ["string", "null"] + }, + "itemId": { + "type": "string" + }, + "permissions": { + "$ref": "#/definitions/RequestPermissionProfile" + }, + "reason": { + "type": ["string", "null"] + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this approval request started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["cwd", "itemId", "permissions", "startedAtMs", "threadId", "turnId"], + "type": "object" + }, + "RequestId": { + "anyOf": [ + { + "type": "string" + }, + { + "format": "int64", + "type": "integer" + } + ] + }, + "RequestPermissionProfile": { + "additionalProperties": false, + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ThreadId": { + "type": "string" + }, + "ToolRequestUserInputOption": { + "description": "EXPERIMENTAL. Defines a single selectable option for request_user_input.", + "properties": { + "description": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": ["description", "label"], + "type": "object" + }, + "ToolRequestUserInputParams": { + "description": "EXPERIMENTAL. Params sent with a request_user_input event.", + "properties": { + "autoResolutionMs": { + "default": null, + "description": "@deprecated Use `isBlocking` to decide whether the request should block.", + "format": "uint64", + "minimum": 0.0, + "type": ["integer", "null"] + }, + "isBlocking": { + "type": "boolean" + }, + "itemId": { + "type": "string" + }, + "questions": { + "items": { + "$ref": "#/definitions/ToolRequestUserInputQuestion" + }, + "type": "array" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["isBlocking", "itemId", "questions", "threadId", "turnId"], + "type": "object" + }, + "ToolRequestUserInputQuestion": { + "description": "EXPERIMENTAL. Represents one request_user_input question and its required options.", + "properties": { + "header": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isOther": { + "default": false, + "type": "boolean" + }, + "isSecret": { + "default": false, + "type": "boolean" + }, + "options": { + "items": { + "$ref": "#/definitions/ToolRequestUserInputOption" + }, + "type": ["array", "null"] + }, + "question": { + "type": "string" + } + }, + "required": ["header", "id", "question"], + "type": "object" + } + }, + "description": "Request initiated from the server and sent to the client.", + "oneOf": [ + { + "description": "NEW APIs Sent when approval is requested for a specific command execution. This request is used for Turns started via turn/start.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["item/commandExecution/requestApproval"], + "title": "Item/commandExecution/requestApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecutionRequestApprovalParams" + } + }, + "required": ["id", "method", "params"], + "title": "Item/commandExecution/requestApprovalRequest", + "type": "object" + }, + { + "description": "Sent when approval is requested for a specific file change. This request is used for Turns started via turn/start.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["item/fileChange/requestApproval"], + "title": "Item/fileChange/requestApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FileChangeRequestApprovalParams" + } + }, + "required": ["id", "method", "params"], + "title": "Item/fileChange/requestApprovalRequest", + "type": "object" + }, + { + "description": "EXPERIMENTAL - Request input from the user for a tool call.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["item/tool/requestUserInput"], + "title": "Item/tool/requestUserInputRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ToolRequestUserInputParams" + } + }, + "required": ["id", "method", "params"], + "title": "Item/tool/requestUserInputRequest", + "type": "object" + }, + { + "description": "Request input for an MCP server elicitation.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["mcpServer/elicitation/request"], + "title": "McpServer/elicitation/requestRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerElicitationRequestParams" + } + }, + "required": ["id", "method", "params"], + "title": "McpServer/elicitation/requestRequest", + "type": "object" + }, + { + "description": "Request approval for additional permissions from the user.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["item/permissions/requestApproval"], + "title": "Item/permissions/requestApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PermissionsRequestApprovalParams" + } + }, + "required": ["id", "method", "params"], + "title": "Item/permissions/requestApprovalRequest", + "type": "object" + }, + { + "description": "Execute a dynamic tool call on the client.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["item/tool/call"], + "title": "Item/tool/callRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/DynamicToolCallParams" + } + }, + "required": ["id", "method", "params"], + "title": "Item/tool/callRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["account/chatgptAuthTokens/refresh"], + "title": "Account/chatgptAuthTokens/refreshRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ChatgptAuthTokensRefreshParams" + } + }, + "required": ["id", "method", "params"], + "title": "Account/chatgptAuthTokens/refreshRequest", + "type": "object" + }, + { + "description": "Generate a fresh upstream attestation result on demand.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["attestation/generate"], + "title": "Attestation/generateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AttestationGenerateParams" + } + }, + "required": ["id", "method", "params"], + "title": "Attestation/generateRequest", + "type": "object" + }, + { + "description": "Read the current time from an external clock owned by the client.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["currentTime/read"], + "title": "CurrentTime/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CurrentTimeReadParams" + } + }, + "required": ["id", "method", "params"], + "title": "CurrentTime/readRequest", + "type": "object" + }, + { + "description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["applyPatchApproval"], + "title": "ApplyPatchApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ApplyPatchApprovalParams" + } + }, + "required": ["id", "method", "params"], + "title": "ApplyPatchApprovalRequest", + "type": "object" + }, + { + "description": "Request to exec a command. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": ["execCommandApproval"], + "title": "ExecCommandApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExecCommandApprovalParams" + } + }, + "required": ["id", "method", "params"], + "title": "ExecCommandApprovalRequest", + "type": "object" + } + ], + "title": "ServerRequest" +} diff --git a/src/runtimes/openai/generated-json-schema/ThreadBackgroundTerminalsCleanResponse.json b/src/runtimes/openai/generated-json-schema/ThreadBackgroundTerminalsCleanResponse.json new file mode 100644 index 0000000..cc1ed20 --- /dev/null +++ b/src/runtimes/openai/generated-json-schema/ThreadBackgroundTerminalsCleanResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadBackgroundTerminalsCleanResponse", + "type": "object" +} diff --git a/src/runtimes/openai/generated-json-schema/ThreadInjectItemsResponse.json b/src/runtimes/openai/generated-json-schema/ThreadInjectItemsResponse.json new file mode 100644 index 0000000..f2835b9 --- /dev/null +++ b/src/runtimes/openai/generated-json-schema/ThreadInjectItemsResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadInjectItemsResponse", + "type": "object" +} diff --git a/src/runtimes/openai/generated-json-schema/ThreadResumeResponse.json b/src/runtimes/openai/generated-json-schema/ThreadResumeResponse.json new file mode 100644 index 0000000..8423516 --- /dev/null +++ b/src/runtimes/openai/generated-json-schema/ThreadResumeResponse.json @@ -0,0 +1,2209 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "ActivePermissionProfile": { + "properties": { + "extends": { + "default": null, + "description": "Parent profile identifier from the selected permissions profile's `extends` setting, when present.", + "type": ["string", "null"] + }, + "id": { + "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", + "type": "string" + } + }, + "required": ["id"], + "type": "object" + }, + "AgentMessageDelivery": { + "enum": ["async"], + "type": "string" + }, + "AgentPath": { + "type": "string" + }, + "ApprovalsReviewer": { + "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", + "enum": ["user", "auto_review", "guardian_subagent"], + "type": "string" + }, + "AskForApproval": { + "oneOf": [ + { + "enum": ["untrusted", "on-request", "never"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "granular": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } + }, + "required": ["mcp_elicitations", "rules", "sandbox_approval"], + "type": "object" + } + }, + "required": ["granular"], + "title": "GranularAskForApproval", + "type": "object" + } + ] + }, + "ByteRange": { + "properties": { + "end": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "start": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": ["end", "start"], + "type": "object" + }, + "CodexErrorInfo": { + "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", + "oneOf": [ + { + "enum": [ + "contextWindowExceeded", + "sessionBudgetExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "misalignmentPolicyViolation", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "httpConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["httpConnectionFailed"], + "title": "HttpConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Failed to connect to the response SSE stream.", + "properties": { + "responseStreamConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseStreamConnectionFailed"], + "title": "ResponseStreamConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "The response SSE stream disconnected in the middle of a turn before completion.", + "properties": { + "responseStreamDisconnected": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseStreamDisconnected"], + "title": "ResponseStreamDisconnectedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reached the retry limit for responses.", + "properties": { + "responseTooManyFailedAttempts": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseTooManyFailedAttempts"], + "title": "ResponseTooManyFailedAttemptsCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", + "properties": { + "activeTurnNotSteerable": { + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + }, + "required": ["turnKind"], + "type": "object" + } + }, + "required": ["activeTurnNotSteerable"], + "title": "ActiveTurnNotSteerableCodexErrorInfo", + "type": "object" + } + ] + }, + "CollabAgentState": { + "properties": { + "message": { + "type": ["string", "null"] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } + }, + "required": ["status"], + "type": "object" + }, + "CollabAgentStatus": { + "enum": [ + "pendingInit", + "running", + "interrupted", + "completed", + "errored", + "shutdown", + "notFound" + ], + "type": "string" + }, + "CollabAgentTool": { + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents" + ], + "type": "string" + }, + "CollabAgentToolCallStatus": { + "enum": ["inProgress", "completed", "failed", "interrupted"], + "type": "string" + }, + "CommandAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["read"], + "title": "ReadCommandActionType", + "type": "string" + } + }, + "required": ["command", "name", "path", "type"], + "title": "ReadCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["listFiles"], + "title": "ListFilesCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "ListFilesCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "SearchCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "type": { + "enum": ["unknown"], + "title": "UnknownCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "UnknownCommandAction", + "type": "object" + } + ] + }, + "CommandExecutionSource": { + "enum": ["agent", "userShell", "unifiedExecStartup", "unifiedExecInteraction"], + "type": "string" + }, + "CommandExecutionStatus": { + "enum": ["inProgress", "completed", "failed", "declined"], + "type": "string" + }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": ["inputText"], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": ["inputImage"], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["imageUrl", "type"], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audioUrl": { + "type": "string" + }, + "type": { + "enum": ["inputAudio"], + "title": "InputAudioDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["audioUrl", "type"], + "title": "InputAudioDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": ["inProgress", "completed", "failed"], + "type": "string" + }, + "FileUpdateChange": { + "properties": { + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } + }, + "required": ["diff", "kind", "path"], + "type": "object" + }, + "GitInfo": { + "properties": { + "branch": { + "type": ["string", "null"] + }, + "originUrl": { + "type": ["string", "null"] + }, + "sha": { + "type": ["string", "null"] + } + }, + "type": "object" + }, + "HookPromptFragment": { + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["hookRunId", "text"], + "type": "object" + }, + "ImageDetail": { + "enum": ["auto", "low", "high", "original"], + "type": "string" + }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": ["integer", "null"] + }, + "type": { + "enum": ["usageLimitExceeded"], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": ["limitId", "type"], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, + "LegacyAppPathString": { + "type": "string" + }, + "McpToolCallAppContext": { + "properties": { + "actionName": { + "type": ["string", "null"] + }, + "appName": { + "type": ["string", "null"] + }, + "connectorId": { + "type": "string" + }, + "linkId": { + "type": ["string", "null"] + }, + "resourceUri": { + "type": ["string", "null"] + } + }, + "required": ["connectorId"], + "type": "object" + }, + "McpToolCallError": { + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, + "McpToolCallResult": { + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "structuredContent": true + }, + "required": ["content"], + "type": "object" + }, + "McpToolCallStatus": { + "enum": ["inProgress", "completed", "failed"], + "type": "string" + }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "threadIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["entries", "threadIds"], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": ["lineEnd", "lineStart", "note", "path"], + "type": "object" + }, + "MessagePhase": { + "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", + "oneOf": [ + { + "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", + "enum": ["commentary"], + "type": "string" + }, + { + "description": "The assistant's terminal answer text for the current turn.", + "enum": ["final_answer"], + "type": "string" + } + ] + }, + "MultiAgentMode": { + "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", + "oneOf": [ + { + "enum": ["explicitRequestOnly", "proactive"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "custom": { + "type": "string" + } + }, + "required": ["custom"], + "title": "CustomMultiAgentMode", + "type": "object" + } + ] + }, + "NetworkAccess": { + "enum": ["restricted", "enabled"], + "type": "string" + }, + "NonSteerableTurnKind": { + "enum": ["review", "compact"], + "type": "string" + }, + "PatchApplyStatus": { + "enum": ["inProgress", "completed", "failed", "declined"], + "type": "string" + }, + "PatchChangeKind": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["add"], + "title": "AddPatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "AddPatchChangeKind", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["delete"], + "title": "DeletePatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "DeletePatchChangeKind", + "type": "object" + }, + { + "properties": { + "move_path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["update"], + "title": "UpdatePatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "UpdatePatchChangeKind", + "type": "object" + } + ] + }, + "ReasoningEffort": { + "description": "A non-empty reasoning effort value advertised by the model.", + "minLength": 1, + "type": "string" + }, + "SandboxPolicy": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["dangerFullAccess"], + "title": "DangerFullAccessSandboxPolicyType", + "type": "string" + } + }, + "required": ["type"], + "title": "DangerFullAccessSandboxPolicy", + "type": "object" + }, + { + "properties": { + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "enum": ["readOnly"], + "title": "ReadOnlySandboxPolicyType", + "type": "string" + } + }, + "required": ["type"], + "title": "ReadOnlySandboxPolicy", + "type": "object" + }, + { + "properties": { + "networkAccess": { + "allOf": [ + { + "$ref": "#/definitions/NetworkAccess" + } + ], + "default": "restricted" + }, + "type": { + "enum": ["externalSandbox"], + "title": "ExternalSandboxSandboxPolicyType", + "type": "string" + } + }, + "required": ["type"], + "title": "ExternalSandboxSandboxPolicy", + "type": "object" + }, + { + "properties": { + "excludeSlashTmp": { + "default": false, + "type": "boolean" + }, + "excludeTmpdirEnvVar": { + "default": false, + "type": "boolean" + }, + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "enum": ["workspaceWrite"], + "title": "WorkspaceWriteSandboxPolicyType", + "type": "string" + }, + "writableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": ["type"], + "title": "WorkspaceWriteSandboxPolicy", + "type": "object" + } + ] + }, + "SessionSource": { + "oneOf": [ + { + "enum": ["cli", "vscode", "exec", "appServer", "unknown"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "custom": { + "type": "string" + } + }, + "required": ["custom"], + "title": "CustomSessionSource", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "subAgent": { + "$ref": "#/definitions/SubAgentSource" + } + }, + "required": ["subAgent"], + "title": "SubAgentSessionSource", + "type": "object" + } + ] + }, + "SubAgentActivityKind": { + "enum": ["started", "interacted", "interrupted", "completed"], + "type": "string" + }, + "SubAgentSource": { + "oneOf": [ + { + "enum": ["review", "compact", "memory_consolidation"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "thread_spawn": { + "properties": { + "agent_nickname": { + "default": null, + "type": ["string", "null"] + }, + "agent_path": { + "anyOf": [ + { + "$ref": "#/definitions/AgentPath" + }, + { + "type": "null" + } + ], + "default": null + }, + "agent_role": { + "default": null, + "type": ["string", "null"] + }, + "depth": { + "format": "int32", + "type": "integer" + }, + "parent_thread_id": { + "$ref": "#/definitions/ThreadId" + } + }, + "required": ["depth", "parent_thread_id"], + "type": "object" + } + }, + "required": ["thread_spawn"], + "title": "ThreadSpawnSubAgentSource", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "other": { + "type": "string" + } + }, + "required": ["other"], + "title": "OtherSubAgentSource", + "type": "object" + } + ] + }, + "TextElement": { + "properties": { + "byteRange": { + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ], + "description": "Byte range in the parent `text` buffer that this element occupies." + }, + "placeholder": { + "description": "Optional human-readable placeholder for the element, displayed in the UI.", + "type": ["string", "null"] + } + }, + "required": ["byteRange"], + "type": "object" + }, + "Thread": { + "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": ["string", "null"] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": ["string", "null"] + }, + "canAcceptDirectInput": { + "description": "Whether the app server accepts direct turn input for this loaded thread. `None` means the capability is unavailable, such as for an unloaded stored thread.", + "type": ["boolean", "null"] + }, + "cliVersion": { + "description": "Version of the CLI that created the thread.", + "type": "string" + }, + "createdAt": { + "description": "Unix timestamp (in seconds) when the thread was created.", + "format": "int64", + "type": "integer" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." + }, + "ephemeral": { + "description": "Whether the thread is ephemeral and should not be materialized on disk.", + "type": "boolean" + }, + "extra": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadExtra" + }, + { + "type": "null" + } + ], + "description": "Optional implementation-specific thread data." + }, + "forkedFromId": { + "description": "Source thread id when this thread was created by forking another thread.", + "type": ["string", "null"] + }, + "gitInfo": { + "anyOf": [ + { + "$ref": "#/definitions/GitInfo" + }, + { + "type": "null" + } + ], + "description": "Optional Git metadata captured when the thread was created." + }, + "historyMode": { + "allOf": [ + { + "$ref": "#/definitions/ThreadHistoryMode" + } + ], + "default": "legacy", + "description": "Persisted thread history contract selected when this thread was created." + }, + "id": { + "description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + "type": "string" + }, + "modelProvider": { + "description": "Model provider used for this thread (for example, 'openai').", + "type": "string" + }, + "name": { + "description": "Optional user-facing thread title.", + "type": ["string", "null"] + }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": ["string", "null"] + }, + "path": { + "description": "[UNSTABLE] Path to the thread on disk.", + "type": ["string", "null"] + }, + "preview": { + "description": "Usually the first user message in the thread, if available.", + "type": "string" + }, + "projectId": { + "description": "Canonical project assignment owned by app-server, if any.", + "type": ["string", "null"] + }, + "recencyAt": { + "description": "Unix timestamp (in seconds) used for thread recency ordering.", + "format": "int64", + "type": ["integer", "null"] + }, + "section": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSection" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The independently persisted section selected for this thread, if any." + }, + "sectionEnteredAt": { + "default": null, + "description": "Unix timestamp in seconds when the thread entered its current section.", + "format": "int64", + "type": ["integer", "null"] + }, + "sessionId": { + "description": "Session id shared by threads that belong to the same session tree.", + "type": "string" + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/SessionSource" + } + ], + "description": "Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.)." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/ThreadStatus" + } + ], + "description": "Current runtime status for the thread." + }, + "threadSource": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ], + "description": "Optional analytics source classification for this thread." + }, + "turns": { + "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "items": { + "$ref": "#/definitions/Turn" + }, + "type": "array" + }, + "updatedAt": { + "description": "Unix timestamp (in seconds) when the thread was last updated.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "cliVersion", + "createdAt", + "cwd", + "ephemeral", + "id", + "modelProvider", + "preview", + "projectId", + "sessionId", + "source", + "status", + "turns", + "updatedAt" + ], + "type": "object" + }, + "ThreadActiveFlag": { + "enum": ["waitingOnApproval", "waitingOnUserInput"], + "type": "string" + }, + "ThreadExtra": { + "description": "Extra app-server data for a thread.", + "type": "object" + }, + "ThreadHistoryMode": { + "enum": ["legacy", "paginated"], + "type": "string" + }, + "ThreadId": { + "type": "string" + }, + "ThreadItem": { + "oneOf": [ + { + "properties": { + "clientId": { + "type": ["string", "null"] + }, + "content": { + "items": { + "$ref": "#/definitions/UserInput" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["userMessage"], + "title": "UserMessageThreadItemType", + "type": "string" + } + }, + "required": ["content", "id", "type"], + "title": "UserMessageThreadItem", + "type": "object" + }, + { + "properties": { + "fragments": { + "items": { + "$ref": "#/definitions/HookPromptFragment" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["hookPrompt"], + "title": "HookPromptThreadItemType", + "type": "string" + } + }, + "required": ["fragments", "id", "type"], + "title": "HookPromptThreadItem", + "type": "object" + }, + { + "properties": { + "delivery": { + "anyOf": [ + { + "$ref": "#/definitions/AgentMessageDelivery" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ], + "default": null + }, + "text": { + "type": "string" + }, + "type": { + "enum": ["agentMessage"], + "title": "AgentMessageThreadItemType", + "type": "string" + } + }, + "required": ["id", "text", "type"], + "title": "AgentMessageThreadItem", + "type": "object" + }, + { + "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "enum": ["plan"], + "title": "PlanThreadItemType", + "type": "string" + } + }, + "required": ["id", "text", "type"], + "title": "PlanThreadItem", + "type": "object" + }, + { + "properties": { + "content": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "enum": ["reasoning"], + "title": "ReasoningThreadItemType", + "type": "string" + } + }, + "required": ["id", "type"], + "title": "ReasoningThreadItem", + "type": "object" + }, + { + "properties": { + "aggregatedOutput": { + "description": "The command's output, aggregated from stdout and stderr.", + "type": ["string", "null"] + }, + "command": { + "description": "The command to be executed.", + "type": "string" + }, + "commandActions": { + "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", + "items": { + "$ref": "#/definitions/CommandAction" + }, + "type": "array" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + } + ], + "description": "The command's working directory." + }, + "durationMs": { + "description": "The duration of the command execution in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "exitCode": { + "description": "The command's exit code.", + "format": "int32", + "type": ["integer", "null"] + }, + "id": { + "type": "string" + }, + "pluginId": { + "default": null, + "description": "Trusted first-party plugin id when this command resolves to one plugin script.", + "type": ["string", "null"] + }, + "processId": { + "description": "Identifier for the underlying PTY process (when available).", + "type": ["string", "null"] + }, + "scriptPath": { + "default": null, + "description": "Safe plugin-relative path when this command resolves to one plugin script.", + "type": ["string", "null"] + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ], + "default": "agent" + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" + }, + "type": { + "enum": ["commandExecution"], + "title": "CommandExecutionThreadItemType", + "type": "string" + } + }, + "required": ["command", "commandActions", "cwd", "id", "status", "type"], + "title": "CommandExecutionThreadItem", + "type": "object" + }, + { + "properties": { + "changes": { + "items": { + "$ref": "#/definitions/FileUpdateChange" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, + "type": { + "enum": ["fileChange"], + "title": "FileChangeThreadItemType", + "type": "string" + } + }, + "required": ["changes", "id", "status", "type"], + "title": "FileChangeThreadItem", + "type": "object" + }, + { + "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, + "arguments": true, + "durationMs": { + "description": "The duration of the MCP tool call in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", + "type": ["string", "null"] + }, + "pluginId": { + "type": ["string", "null"] + }, + "readOnlyHint": { + "type": ["boolean", "null"] + }, + "result": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" + }, + "type": { + "enum": ["mcpToolCall"], + "title": "McpToolCallThreadItemType", + "type": "string" + } + }, + "required": ["arguments", "id", "server", "status", "tool", "type"], + "title": "McpToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": ["array", "null"] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "id": { + "type": "string" + }, + "namespace": { + "type": ["string", "null"] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": ["boolean", "null"] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": ["dynamicToolCall"], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": ["arguments", "id", "status", "tool", "type"], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentsStates": { + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + }, + "description": "Last known status of the target agents, when available.", + "type": "object" + }, + "id": { + "description": "Unique identifier for this collab tool call.", + "type": "string" + }, + "model": { + "description": "Model requested for the spawned agent, when applicable.", + "type": ["string", "null"] + }, + "prompt": { + "description": "Prompt text sent as part of the collab tool call, when available.", + "type": ["string", "null"] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Reasoning effort requested for the spawned agent, when applicable." + }, + "receiverThreadIds": { + "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", + "items": { + "type": "string" + }, + "type": "array" + }, + "senderThreadId": { + "description": "Thread ID of the agent issuing the collab request.", + "type": "string" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ], + "description": "Current status of the collab tool call." + }, + "tool": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ], + "description": "Name of the collab tool that was invoked." + }, + "type": { + "enum": ["collabAgentToolCall"], + "title": "CollabAgentToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "agentsStates", + "id", + "receiverThreadIds", + "senderThreadId", + "status", + "tool", + "type" + ], + "title": "CollabAgentToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentPath": { + "type": "string" + }, + "agentThreadId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/SubAgentActivityKind" + }, + "type": { + "enum": ["subAgentActivity"], + "title": "SubAgentActivityThreadItemType", + "type": "string" + } + }, + "required": ["agentPath", "agentThreadId", "id", "kind", "type"], + "title": "SubAgentActivityThreadItem", + "type": "object" + }, + { + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "results": { + "default": null, + "description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + "items": true, + "type": ["array", "null"] + }, + "type": { + "enum": ["webSearch"], + "title": "WebSearchThreadItemType", + "type": "string" + } + }, + "required": ["id", "query", "type"], + "title": "WebSearchThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["imageView"], + "title": "ImageViewThreadItemType", + "type": "string" + } + }, + "required": ["id", "path", "type"], + "title": "ImageViewThreadItem", + "type": "object" + }, + { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", + "properties": { + "durationMs": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["sleep"], + "title": "SleepThreadItemType", + "type": "string" + } + }, + "required": ["durationMs", "id", "type"], + "title": "SleepThreadItem", + "type": "object" + }, + { + "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": ["string", "null"] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" + }, + "transparentBackground": { + "default": null, + "type": ["boolean", "null"] + }, + "type": { + "enum": ["imageGeneration"], + "title": "ImageGenerationThreadItemType", + "type": "string" + } + }, + "required": ["id", "result", "status", "type"], + "title": "ImageGenerationThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": ["enteredReviewMode"], + "title": "EnteredReviewModeThreadItemType", + "type": "string" + } + }, + "required": ["id", "review", "type"], + "title": "EnteredReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": ["exitedReviewMode"], + "title": "ExitedReviewModeThreadItemType", + "type": "string" + } + }, + "required": ["id", "review", "type"], + "title": "ExitedReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "type": { + "enum": ["contextCompaction"], + "title": "ContextCompactionThreadItemType", + "type": "string" + } + }, + "required": ["id", "type"], + "title": "ContextCompactionThreadItem", + "type": "object" + } + ] + }, + "ThreadSection": { + "description": "An independently persisted, user-visible thread section.", + "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, + "id": { + "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", + "type": "string" + }, + "name": { + "description": "The current user-visible section name.", + "type": "string" + } + }, + "required": ["id", "name"], + "type": "object" + }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": ["string", "null"] + }, + "icon": { + "type": ["string", "null"] + } + }, + "type": "object" + }, + "ThreadSource": { + "type": "string" + }, + "ThreadStatus": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["notLoaded"], + "title": "NotLoadedThreadStatusType", + "type": "string" + } + }, + "required": ["type"], + "title": "NotLoadedThreadStatus", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["idle"], + "title": "IdleThreadStatusType", + "type": "string" + } + }, + "required": ["type"], + "title": "IdleThreadStatus", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["systemError"], + "title": "SystemErrorThreadStatusType", + "type": "string" + } + }, + "required": ["type"], + "title": "SystemErrorThreadStatus", + "type": "object" + }, + { + "properties": { + "activeFlags": { + "items": { + "$ref": "#/definitions/ThreadActiveFlag" + }, + "type": "array" + }, + "type": { + "enum": ["active"], + "title": "ActiveThreadStatusType", + "type": "string" + } + }, + "required": ["activeFlags", "type"], + "title": "ActiveThreadStatus", + "type": "object" + } + ] + }, + "Turn": { + "properties": { + "completedAt": { + "description": "Unix timestamp (in seconds) when the turn completed.", + "format": "int64", + "type": ["integer", "null"] + }, + "durationMs": { + "description": "Duration between turn start and completion in milliseconds, if known.", + "format": "int64", + "type": ["integer", "null"] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ], + "description": "Only populated when the Turn's status is failed." + }, + "id": { + "description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + "type": "string" + }, + "items": { + "description": "Thread items currently included in this turn payload.", + "items": { + "$ref": "#/definitions/ThreadItem" + }, + "type": "array" + }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, + "startedAt": { + "description": "Unix timestamp (in seconds) when the turn started.", + "format": "int64", + "type": ["integer", "null"] + }, + "status": { + "$ref": "#/definitions/TurnStatus" + } + }, + "required": ["id", "items", "status"], + "type": "object" + }, + "TurnError": { + "properties": { + "additionalDetails": { + "default": null, + "type": ["string", "null"] + }, + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": ["notLoaded"], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": ["summary"], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": ["full"], + "type": "string" + } + ] + }, + "TurnStatus": { + "enum": ["completed", "interrupted", "failed", "inProgress"], + "type": "string" + }, + "TurnsPage": { + "properties": { + "backwardsCursor": { + "type": ["string", "null"] + }, + "data": { + "items": { + "$ref": "#/definitions/Turn" + }, + "type": "array" + }, + "nextCursor": { + "type": ["string", "null"] + } + }, + "required": ["data"], + "type": "object" + }, + "UserInput": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "text_elements": { + "default": [], + "description": "UI-defined spans within `text` used to render or persist special elements.", + "items": { + "$ref": "#/definitions/TextElement" + }, + "type": "array" + }, + "type": { + "enum": ["text"], + "title": "TextUserInputType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "TextUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "type": { + "enum": ["image"], + "title": "ImageUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["type", "url"], + "title": "ImageUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["localImage"], + "title": "LocalImageUserInputType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "LocalImageUserInput", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["audio"], + "title": "AudioUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["type", "url"], + "title": "AudioUserInput", + "type": "object" + }, + { + "properties": { + "path": { + "type": "string" + }, + "type": { + "enum": ["localAudio"], + "title": "LocalAudioUserInputType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "LocalAudioUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["skill"], + "title": "SkillUserInputType", + "type": "string" + } + }, + "required": ["name", "path", "type"], + "title": "SkillUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["mention"], + "title": "MentionUserInputType", + "type": "string" + } + }, + "required": ["name", "path", "type"], + "title": "MentionUserInput", + "type": "object" + } + ] + }, + "WebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchWebSearchActionType", + "type": "string" + } + }, + "required": ["type"], + "title": "SearchWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["openPage"], + "title": "OpenPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": ["string", "null"] + } + }, + "required": ["type"], + "title": "OpenPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": ["string", "null"] + }, + "type": { + "enum": ["findInPage"], + "title": "FindInPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": ["string", "null"] + } + }, + "required": ["type"], + "title": "FindInPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["other"], + "title": "OtherWebSearchActionType", + "type": "string" + } + }, + "required": ["type"], + "title": "OtherWebSearchAction", + "type": "object" + } + ] + } + }, + "properties": { + "activePermissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/ActivePermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Named or implicit built-in profile that produced the active permissions, when known." + }, + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" + }, + "approvalsReviewer": { + "allOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + } + ], + "description": "Reviewer currently used for approval requests on this thread." + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "initialTurnsPage": { + "anyOf": [ + { + "$ref": "#/definitions/TurnsPage" + }, + { + "type": "null" + } + ], + "default": null, + "description": "`thread/turns/list` page returned when requested by `initialTurnsPage`." + }, + "instructionSources": { + "default": [], + "description": "Environment-native paths to instruction source files currently loaded for this thread.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + }, + "itemsBackwardsCursor": { + "default": null, + "description": "Opaque cursor for hydrating paginated items backwards.\n\nPass this as `cursor` to `thread/items/list` with `sortDirection: \"desc\"`. The first page includes the item identified by the cursor.", + "type": ["string", "null"] + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "multiAgentMode": { + "allOf": [ + { + "$ref": "#/definitions/MultiAgentMode" + } + ], + "default": "explicitRequestOnly", + "description": "@deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior." + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "runtimeWorkspaceRoots": { + "default": [], + "description": "Thread-scoped runtime workspace roots used to materialize `:workspace_roots`.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "sandbox": { + "allOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + } + ], + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." + }, + "serviceTier": { + "type": ["string", "null"] + }, + "thread": { + "$ref": "#/definitions/Thread" + }, + "turnsBackwardsCursor": { + "default": null, + "description": "Opaque cursor for hydrating paginated turns backwards.\n\nPass this as `cursor` to `thread/turns/list` with `sortDirection: \"desc\"`. The first page includes the turn identified by the cursor.", + "type": ["string", "null"] + } + }, + "required": [ + "approvalPolicy", + "approvalsReviewer", + "cwd", + "model", + "modelProvider", + "sandbox", + "thread" + ], + "title": "ThreadResumeResponse", + "type": "object" +} diff --git a/src/runtimes/openai/generated-json-schema/ThreadStartResponse.json b/src/runtimes/openai/generated-json-schema/ThreadStartResponse.json new file mode 100644 index 0000000..dfd36f3 --- /dev/null +++ b/src/runtimes/openai/generated-json-schema/ThreadStartResponse.json @@ -0,0 +1,2169 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "ActivePermissionProfile": { + "properties": { + "extends": { + "default": null, + "description": "Parent profile identifier from the selected permissions profile's `extends` setting, when present.", + "type": ["string", "null"] + }, + "id": { + "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", + "type": "string" + } + }, + "required": ["id"], + "type": "object" + }, + "AgentMessageDelivery": { + "enum": ["async"], + "type": "string" + }, + "AgentPath": { + "type": "string" + }, + "ApprovalsReviewer": { + "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", + "enum": ["user", "auto_review", "guardian_subagent"], + "type": "string" + }, + "AskForApproval": { + "oneOf": [ + { + "enum": ["untrusted", "on-request", "never"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "granular": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } + }, + "required": ["mcp_elicitations", "rules", "sandbox_approval"], + "type": "object" + } + }, + "required": ["granular"], + "title": "GranularAskForApproval", + "type": "object" + } + ] + }, + "ByteRange": { + "properties": { + "end": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "start": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": ["end", "start"], + "type": "object" + }, + "CodexErrorInfo": { + "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", + "oneOf": [ + { + "enum": [ + "contextWindowExceeded", + "sessionBudgetExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "misalignmentPolicyViolation", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "httpConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["httpConnectionFailed"], + "title": "HttpConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Failed to connect to the response SSE stream.", + "properties": { + "responseStreamConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseStreamConnectionFailed"], + "title": "ResponseStreamConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "The response SSE stream disconnected in the middle of a turn before completion.", + "properties": { + "responseStreamDisconnected": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseStreamDisconnected"], + "title": "ResponseStreamDisconnectedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reached the retry limit for responses.", + "properties": { + "responseTooManyFailedAttempts": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseTooManyFailedAttempts"], + "title": "ResponseTooManyFailedAttemptsCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", + "properties": { + "activeTurnNotSteerable": { + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + }, + "required": ["turnKind"], + "type": "object" + } + }, + "required": ["activeTurnNotSteerable"], + "title": "ActiveTurnNotSteerableCodexErrorInfo", + "type": "object" + } + ] + }, + "CollabAgentState": { + "properties": { + "message": { + "type": ["string", "null"] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } + }, + "required": ["status"], + "type": "object" + }, + "CollabAgentStatus": { + "enum": [ + "pendingInit", + "running", + "interrupted", + "completed", + "errored", + "shutdown", + "notFound" + ], + "type": "string" + }, + "CollabAgentTool": { + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents" + ], + "type": "string" + }, + "CollabAgentToolCallStatus": { + "enum": ["inProgress", "completed", "failed", "interrupted"], + "type": "string" + }, + "CommandAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["read"], + "title": "ReadCommandActionType", + "type": "string" + } + }, + "required": ["command", "name", "path", "type"], + "title": "ReadCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["listFiles"], + "title": "ListFilesCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "ListFilesCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "SearchCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "type": { + "enum": ["unknown"], + "title": "UnknownCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "UnknownCommandAction", + "type": "object" + } + ] + }, + "CommandExecutionSource": { + "enum": ["agent", "userShell", "unifiedExecStartup", "unifiedExecInteraction"], + "type": "string" + }, + "CommandExecutionStatus": { + "enum": ["inProgress", "completed", "failed", "declined"], + "type": "string" + }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": ["inputText"], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": ["inputImage"], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["imageUrl", "type"], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audioUrl": { + "type": "string" + }, + "type": { + "enum": ["inputAudio"], + "title": "InputAudioDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["audioUrl", "type"], + "title": "InputAudioDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": ["inProgress", "completed", "failed"], + "type": "string" + }, + "FileUpdateChange": { + "properties": { + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } + }, + "required": ["diff", "kind", "path"], + "type": "object" + }, + "GitInfo": { + "properties": { + "branch": { + "type": ["string", "null"] + }, + "originUrl": { + "type": ["string", "null"] + }, + "sha": { + "type": ["string", "null"] + } + }, + "type": "object" + }, + "HookPromptFragment": { + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["hookRunId", "text"], + "type": "object" + }, + "ImageDetail": { + "enum": ["auto", "low", "high", "original"], + "type": "string" + }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": ["integer", "null"] + }, + "type": { + "enum": ["usageLimitExceeded"], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": ["limitId", "type"], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, + "LegacyAppPathString": { + "type": "string" + }, + "McpToolCallAppContext": { + "properties": { + "actionName": { + "type": ["string", "null"] + }, + "appName": { + "type": ["string", "null"] + }, + "connectorId": { + "type": "string" + }, + "linkId": { + "type": ["string", "null"] + }, + "resourceUri": { + "type": ["string", "null"] + } + }, + "required": ["connectorId"], + "type": "object" + }, + "McpToolCallError": { + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, + "McpToolCallResult": { + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "structuredContent": true + }, + "required": ["content"], + "type": "object" + }, + "McpToolCallStatus": { + "enum": ["inProgress", "completed", "failed"], + "type": "string" + }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "threadIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["entries", "threadIds"], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": ["lineEnd", "lineStart", "note", "path"], + "type": "object" + }, + "MessagePhase": { + "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", + "oneOf": [ + { + "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", + "enum": ["commentary"], + "type": "string" + }, + { + "description": "The assistant's terminal answer text for the current turn.", + "enum": ["final_answer"], + "type": "string" + } + ] + }, + "MultiAgentMode": { + "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", + "oneOf": [ + { + "enum": ["explicitRequestOnly", "proactive"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "custom": { + "type": "string" + } + }, + "required": ["custom"], + "title": "CustomMultiAgentMode", + "type": "object" + } + ] + }, + "NetworkAccess": { + "enum": ["restricted", "enabled"], + "type": "string" + }, + "NonSteerableTurnKind": { + "enum": ["review", "compact"], + "type": "string" + }, + "PatchApplyStatus": { + "enum": ["inProgress", "completed", "failed", "declined"], + "type": "string" + }, + "PatchChangeKind": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["add"], + "title": "AddPatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "AddPatchChangeKind", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["delete"], + "title": "DeletePatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "DeletePatchChangeKind", + "type": "object" + }, + { + "properties": { + "move_path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["update"], + "title": "UpdatePatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "UpdatePatchChangeKind", + "type": "object" + } + ] + }, + "ReasoningEffort": { + "description": "A non-empty reasoning effort value advertised by the model.", + "minLength": 1, + "type": "string" + }, + "SandboxPolicy": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["dangerFullAccess"], + "title": "DangerFullAccessSandboxPolicyType", + "type": "string" + } + }, + "required": ["type"], + "title": "DangerFullAccessSandboxPolicy", + "type": "object" + }, + { + "properties": { + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "enum": ["readOnly"], + "title": "ReadOnlySandboxPolicyType", + "type": "string" + } + }, + "required": ["type"], + "title": "ReadOnlySandboxPolicy", + "type": "object" + }, + { + "properties": { + "networkAccess": { + "allOf": [ + { + "$ref": "#/definitions/NetworkAccess" + } + ], + "default": "restricted" + }, + "type": { + "enum": ["externalSandbox"], + "title": "ExternalSandboxSandboxPolicyType", + "type": "string" + } + }, + "required": ["type"], + "title": "ExternalSandboxSandboxPolicy", + "type": "object" + }, + { + "properties": { + "excludeSlashTmp": { + "default": false, + "type": "boolean" + }, + "excludeTmpdirEnvVar": { + "default": false, + "type": "boolean" + }, + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "enum": ["workspaceWrite"], + "title": "WorkspaceWriteSandboxPolicyType", + "type": "string" + }, + "writableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": ["type"], + "title": "WorkspaceWriteSandboxPolicy", + "type": "object" + } + ] + }, + "SessionSource": { + "oneOf": [ + { + "enum": ["cli", "vscode", "exec", "appServer", "unknown"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "custom": { + "type": "string" + } + }, + "required": ["custom"], + "title": "CustomSessionSource", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "subAgent": { + "$ref": "#/definitions/SubAgentSource" + } + }, + "required": ["subAgent"], + "title": "SubAgentSessionSource", + "type": "object" + } + ] + }, + "SubAgentActivityKind": { + "enum": ["started", "interacted", "interrupted", "completed"], + "type": "string" + }, + "SubAgentSource": { + "oneOf": [ + { + "enum": ["review", "compact", "memory_consolidation"], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "thread_spawn": { + "properties": { + "agent_nickname": { + "default": null, + "type": ["string", "null"] + }, + "agent_path": { + "anyOf": [ + { + "$ref": "#/definitions/AgentPath" + }, + { + "type": "null" + } + ], + "default": null + }, + "agent_role": { + "default": null, + "type": ["string", "null"] + }, + "depth": { + "format": "int32", + "type": "integer" + }, + "parent_thread_id": { + "$ref": "#/definitions/ThreadId" + } + }, + "required": ["depth", "parent_thread_id"], + "type": "object" + } + }, + "required": ["thread_spawn"], + "title": "ThreadSpawnSubAgentSource", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "other": { + "type": "string" + } + }, + "required": ["other"], + "title": "OtherSubAgentSource", + "type": "object" + } + ] + }, + "TextElement": { + "properties": { + "byteRange": { + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ], + "description": "Byte range in the parent `text` buffer that this element occupies." + }, + "placeholder": { + "description": "Optional human-readable placeholder for the element, displayed in the UI.", + "type": ["string", "null"] + } + }, + "required": ["byteRange"], + "type": "object" + }, + "Thread": { + "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": ["string", "null"] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": ["string", "null"] + }, + "canAcceptDirectInput": { + "description": "Whether the app server accepts direct turn input for this loaded thread. `None` means the capability is unavailable, such as for an unloaded stored thread.", + "type": ["boolean", "null"] + }, + "cliVersion": { + "description": "Version of the CLI that created the thread.", + "type": "string" + }, + "createdAt": { + "description": "Unix timestamp (in seconds) when the thread was created.", + "format": "int64", + "type": "integer" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." + }, + "ephemeral": { + "description": "Whether the thread is ephemeral and should not be materialized on disk.", + "type": "boolean" + }, + "extra": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadExtra" + }, + { + "type": "null" + } + ], + "description": "Optional implementation-specific thread data." + }, + "forkedFromId": { + "description": "Source thread id when this thread was created by forking another thread.", + "type": ["string", "null"] + }, + "gitInfo": { + "anyOf": [ + { + "$ref": "#/definitions/GitInfo" + }, + { + "type": "null" + } + ], + "description": "Optional Git metadata captured when the thread was created." + }, + "historyMode": { + "allOf": [ + { + "$ref": "#/definitions/ThreadHistoryMode" + } + ], + "default": "legacy", + "description": "Persisted thread history contract selected when this thread was created." + }, + "id": { + "description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + "type": "string" + }, + "modelProvider": { + "description": "Model provider used for this thread (for example, 'openai').", + "type": "string" + }, + "name": { + "description": "Optional user-facing thread title.", + "type": ["string", "null"] + }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": ["string", "null"] + }, + "path": { + "description": "[UNSTABLE] Path to the thread on disk.", + "type": ["string", "null"] + }, + "preview": { + "description": "Usually the first user message in the thread, if available.", + "type": "string" + }, + "projectId": { + "description": "Canonical project assignment owned by app-server, if any.", + "type": ["string", "null"] + }, + "recencyAt": { + "description": "Unix timestamp (in seconds) used for thread recency ordering.", + "format": "int64", + "type": ["integer", "null"] + }, + "section": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSection" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The independently persisted section selected for this thread, if any." + }, + "sectionEnteredAt": { + "default": null, + "description": "Unix timestamp in seconds when the thread entered its current section.", + "format": "int64", + "type": ["integer", "null"] + }, + "sessionId": { + "description": "Session id shared by threads that belong to the same session tree.", + "type": "string" + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/SessionSource" + } + ], + "description": "Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.)." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/ThreadStatus" + } + ], + "description": "Current runtime status for the thread." + }, + "threadSource": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ], + "description": "Optional analytics source classification for this thread." + }, + "turns": { + "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "items": { + "$ref": "#/definitions/Turn" + }, + "type": "array" + }, + "updatedAt": { + "description": "Unix timestamp (in seconds) when the thread was last updated.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "cliVersion", + "createdAt", + "cwd", + "ephemeral", + "id", + "modelProvider", + "preview", + "projectId", + "sessionId", + "source", + "status", + "turns", + "updatedAt" + ], + "type": "object" + }, + "ThreadActiveFlag": { + "enum": ["waitingOnApproval", "waitingOnUserInput"], + "type": "string" + }, + "ThreadExtra": { + "description": "Extra app-server data for a thread.", + "type": "object" + }, + "ThreadHistoryMode": { + "enum": ["legacy", "paginated"], + "type": "string" + }, + "ThreadId": { + "type": "string" + }, + "ThreadItem": { + "oneOf": [ + { + "properties": { + "clientId": { + "type": ["string", "null"] + }, + "content": { + "items": { + "$ref": "#/definitions/UserInput" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["userMessage"], + "title": "UserMessageThreadItemType", + "type": "string" + } + }, + "required": ["content", "id", "type"], + "title": "UserMessageThreadItem", + "type": "object" + }, + { + "properties": { + "fragments": { + "items": { + "$ref": "#/definitions/HookPromptFragment" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["hookPrompt"], + "title": "HookPromptThreadItemType", + "type": "string" + } + }, + "required": ["fragments", "id", "type"], + "title": "HookPromptThreadItem", + "type": "object" + }, + { + "properties": { + "delivery": { + "anyOf": [ + { + "$ref": "#/definitions/AgentMessageDelivery" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ], + "default": null + }, + "text": { + "type": "string" + }, + "type": { + "enum": ["agentMessage"], + "title": "AgentMessageThreadItemType", + "type": "string" + } + }, + "required": ["id", "text", "type"], + "title": "AgentMessageThreadItem", + "type": "object" + }, + { + "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "enum": ["plan"], + "title": "PlanThreadItemType", + "type": "string" + } + }, + "required": ["id", "text", "type"], + "title": "PlanThreadItem", + "type": "object" + }, + { + "properties": { + "content": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "enum": ["reasoning"], + "title": "ReasoningThreadItemType", + "type": "string" + } + }, + "required": ["id", "type"], + "title": "ReasoningThreadItem", + "type": "object" + }, + { + "properties": { + "aggregatedOutput": { + "description": "The command's output, aggregated from stdout and stderr.", + "type": ["string", "null"] + }, + "command": { + "description": "The command to be executed.", + "type": "string" + }, + "commandActions": { + "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", + "items": { + "$ref": "#/definitions/CommandAction" + }, + "type": "array" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + } + ], + "description": "The command's working directory." + }, + "durationMs": { + "description": "The duration of the command execution in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "exitCode": { + "description": "The command's exit code.", + "format": "int32", + "type": ["integer", "null"] + }, + "id": { + "type": "string" + }, + "pluginId": { + "default": null, + "description": "Trusted first-party plugin id when this command resolves to one plugin script.", + "type": ["string", "null"] + }, + "processId": { + "description": "Identifier for the underlying PTY process (when available).", + "type": ["string", "null"] + }, + "scriptPath": { + "default": null, + "description": "Safe plugin-relative path when this command resolves to one plugin script.", + "type": ["string", "null"] + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ], + "default": "agent" + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" + }, + "type": { + "enum": ["commandExecution"], + "title": "CommandExecutionThreadItemType", + "type": "string" + } + }, + "required": ["command", "commandActions", "cwd", "id", "status", "type"], + "title": "CommandExecutionThreadItem", + "type": "object" + }, + { + "properties": { + "changes": { + "items": { + "$ref": "#/definitions/FileUpdateChange" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, + "type": { + "enum": ["fileChange"], + "title": "FileChangeThreadItemType", + "type": "string" + } + }, + "required": ["changes", "id", "status", "type"], + "title": "FileChangeThreadItem", + "type": "object" + }, + { + "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, + "arguments": true, + "durationMs": { + "description": "The duration of the MCP tool call in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", + "type": ["string", "null"] + }, + "pluginId": { + "type": ["string", "null"] + }, + "readOnlyHint": { + "type": ["boolean", "null"] + }, + "result": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" + }, + "type": { + "enum": ["mcpToolCall"], + "title": "McpToolCallThreadItemType", + "type": "string" + } + }, + "required": ["arguments", "id", "server", "status", "tool", "type"], + "title": "McpToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": ["array", "null"] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "id": { + "type": "string" + }, + "namespace": { + "type": ["string", "null"] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": ["boolean", "null"] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": ["dynamicToolCall"], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": ["arguments", "id", "status", "tool", "type"], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentsStates": { + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + }, + "description": "Last known status of the target agents, when available.", + "type": "object" + }, + "id": { + "description": "Unique identifier for this collab tool call.", + "type": "string" + }, + "model": { + "description": "Model requested for the spawned agent, when applicable.", + "type": ["string", "null"] + }, + "prompt": { + "description": "Prompt text sent as part of the collab tool call, when available.", + "type": ["string", "null"] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Reasoning effort requested for the spawned agent, when applicable." + }, + "receiverThreadIds": { + "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", + "items": { + "type": "string" + }, + "type": "array" + }, + "senderThreadId": { + "description": "Thread ID of the agent issuing the collab request.", + "type": "string" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ], + "description": "Current status of the collab tool call." + }, + "tool": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ], + "description": "Name of the collab tool that was invoked." + }, + "type": { + "enum": ["collabAgentToolCall"], + "title": "CollabAgentToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "agentsStates", + "id", + "receiverThreadIds", + "senderThreadId", + "status", + "tool", + "type" + ], + "title": "CollabAgentToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentPath": { + "type": "string" + }, + "agentThreadId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/SubAgentActivityKind" + }, + "type": { + "enum": ["subAgentActivity"], + "title": "SubAgentActivityThreadItemType", + "type": "string" + } + }, + "required": ["agentPath", "agentThreadId", "id", "kind", "type"], + "title": "SubAgentActivityThreadItem", + "type": "object" + }, + { + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "results": { + "default": null, + "description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + "items": true, + "type": ["array", "null"] + }, + "type": { + "enum": ["webSearch"], + "title": "WebSearchThreadItemType", + "type": "string" + } + }, + "required": ["id", "query", "type"], + "title": "WebSearchThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["imageView"], + "title": "ImageViewThreadItemType", + "type": "string" + } + }, + "required": ["id", "path", "type"], + "title": "ImageViewThreadItem", + "type": "object" + }, + { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", + "properties": { + "durationMs": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["sleep"], + "title": "SleepThreadItemType", + "type": "string" + } + }, + "required": ["durationMs", "id", "type"], + "title": "SleepThreadItem", + "type": "object" + }, + { + "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": ["string", "null"] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" + }, + "transparentBackground": { + "default": null, + "type": ["boolean", "null"] + }, + "type": { + "enum": ["imageGeneration"], + "title": "ImageGenerationThreadItemType", + "type": "string" + } + }, + "required": ["id", "result", "status", "type"], + "title": "ImageGenerationThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": ["enteredReviewMode"], + "title": "EnteredReviewModeThreadItemType", + "type": "string" + } + }, + "required": ["id", "review", "type"], + "title": "EnteredReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": ["exitedReviewMode"], + "title": "ExitedReviewModeThreadItemType", + "type": "string" + } + }, + "required": ["id", "review", "type"], + "title": "ExitedReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "type": { + "enum": ["contextCompaction"], + "title": "ContextCompactionThreadItemType", + "type": "string" + } + }, + "required": ["id", "type"], + "title": "ContextCompactionThreadItem", + "type": "object" + } + ] + }, + "ThreadSection": { + "description": "An independently persisted, user-visible thread section.", + "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, + "id": { + "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", + "type": "string" + }, + "name": { + "description": "The current user-visible section name.", + "type": "string" + } + }, + "required": ["id", "name"], + "type": "object" + }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": ["string", "null"] + }, + "icon": { + "type": ["string", "null"] + } + }, + "type": "object" + }, + "ThreadSource": { + "type": "string" + }, + "ThreadStatus": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["notLoaded"], + "title": "NotLoadedThreadStatusType", + "type": "string" + } + }, + "required": ["type"], + "title": "NotLoadedThreadStatus", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["idle"], + "title": "IdleThreadStatusType", + "type": "string" + } + }, + "required": ["type"], + "title": "IdleThreadStatus", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["systemError"], + "title": "SystemErrorThreadStatusType", + "type": "string" + } + }, + "required": ["type"], + "title": "SystemErrorThreadStatus", + "type": "object" + }, + { + "properties": { + "activeFlags": { + "items": { + "$ref": "#/definitions/ThreadActiveFlag" + }, + "type": "array" + }, + "type": { + "enum": ["active"], + "title": "ActiveThreadStatusType", + "type": "string" + } + }, + "required": ["activeFlags", "type"], + "title": "ActiveThreadStatus", + "type": "object" + } + ] + }, + "Turn": { + "properties": { + "completedAt": { + "description": "Unix timestamp (in seconds) when the turn completed.", + "format": "int64", + "type": ["integer", "null"] + }, + "durationMs": { + "description": "Duration between turn start and completion in milliseconds, if known.", + "format": "int64", + "type": ["integer", "null"] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ], + "description": "Only populated when the Turn's status is failed." + }, + "id": { + "description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + "type": "string" + }, + "items": { + "description": "Thread items currently included in this turn payload.", + "items": { + "$ref": "#/definitions/ThreadItem" + }, + "type": "array" + }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, + "startedAt": { + "description": "Unix timestamp (in seconds) when the turn started.", + "format": "int64", + "type": ["integer", "null"] + }, + "status": { + "$ref": "#/definitions/TurnStatus" + } + }, + "required": ["id", "items", "status"], + "type": "object" + }, + "TurnError": { + "properties": { + "additionalDetails": { + "default": null, + "type": ["string", "null"] + }, + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": ["notLoaded"], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": ["summary"], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": ["full"], + "type": "string" + } + ] + }, + "TurnStatus": { + "enum": ["completed", "interrupted", "failed", "inProgress"], + "type": "string" + }, + "UserInput": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "text_elements": { + "default": [], + "description": "UI-defined spans within `text` used to render or persist special elements.", + "items": { + "$ref": "#/definitions/TextElement" + }, + "type": "array" + }, + "type": { + "enum": ["text"], + "title": "TextUserInputType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "TextUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "type": { + "enum": ["image"], + "title": "ImageUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["type", "url"], + "title": "ImageUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["localImage"], + "title": "LocalImageUserInputType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "LocalImageUserInput", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["audio"], + "title": "AudioUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["type", "url"], + "title": "AudioUserInput", + "type": "object" + }, + { + "properties": { + "path": { + "type": "string" + }, + "type": { + "enum": ["localAudio"], + "title": "LocalAudioUserInputType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "LocalAudioUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["skill"], + "title": "SkillUserInputType", + "type": "string" + } + }, + "required": ["name", "path", "type"], + "title": "SkillUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["mention"], + "title": "MentionUserInputType", + "type": "string" + } + }, + "required": ["name", "path", "type"], + "title": "MentionUserInput", + "type": "object" + } + ] + }, + "WebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchWebSearchActionType", + "type": "string" + } + }, + "required": ["type"], + "title": "SearchWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["openPage"], + "title": "OpenPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": ["string", "null"] + } + }, + "required": ["type"], + "title": "OpenPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": ["string", "null"] + }, + "type": { + "enum": ["findInPage"], + "title": "FindInPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": ["string", "null"] + } + }, + "required": ["type"], + "title": "FindInPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["other"], + "title": "OtherWebSearchActionType", + "type": "string" + } + }, + "required": ["type"], + "title": "OtherWebSearchAction", + "type": "object" + } + ] + } + }, + "properties": { + "activePermissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/ActivePermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Named or implicit built-in profile that produced the active permissions, when known." + }, + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" + }, + "approvalsReviewer": { + "allOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + } + ], + "description": "Reviewer currently used for approval requests on this thread." + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "instructionSources": { + "default": [], + "description": "Environment-native paths to instruction source files currently loaded for this thread.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "multiAgentMode": { + "allOf": [ + { + "$ref": "#/definitions/MultiAgentMode" + } + ], + "default": "explicitRequestOnly", + "description": "@deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior." + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "runtimeWorkspaceRoots": { + "default": [], + "description": "Thread-scoped runtime workspace roots used to materialize `:workspace_roots`.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "sandbox": { + "allOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + } + ], + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." + }, + "serviceTier": { + "type": ["string", "null"] + }, + "thread": { + "$ref": "#/definitions/Thread" + } + }, + "required": [ + "approvalPolicy", + "approvalsReviewer", + "cwd", + "model", + "modelProvider", + "sandbox", + "thread" + ], + "title": "ThreadStartResponse", + "type": "object" +} diff --git a/src/runtimes/openai/generated-json-schema/TurnStartResponse.json b/src/runtimes/openai/generated-json-schema/TurnStartResponse.json new file mode 100644 index 0000000..36d0c3a --- /dev/null +++ b/src/runtimes/openai/generated-json-schema/TurnStartResponse.json @@ -0,0 +1,1514 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "AgentMessageDelivery": { + "enum": ["async"], + "type": "string" + }, + "ByteRange": { + "properties": { + "end": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "start": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": ["end", "start"], + "type": "object" + }, + "CodexErrorInfo": { + "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", + "oneOf": [ + { + "enum": [ + "contextWindowExceeded", + "sessionBudgetExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "misalignmentPolicyViolation", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "httpConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["httpConnectionFailed"], + "title": "HttpConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Failed to connect to the response SSE stream.", + "properties": { + "responseStreamConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseStreamConnectionFailed"], + "title": "ResponseStreamConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "The response SSE stream disconnected in the middle of a turn before completion.", + "properties": { + "responseStreamDisconnected": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseStreamDisconnected"], + "title": "ResponseStreamDisconnectedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reached the retry limit for responses.", + "properties": { + "responseTooManyFailedAttempts": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": ["integer", "null"] + } + }, + "type": "object" + } + }, + "required": ["responseTooManyFailedAttempts"], + "title": "ResponseTooManyFailedAttemptsCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", + "properties": { + "activeTurnNotSteerable": { + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + }, + "required": ["turnKind"], + "type": "object" + } + }, + "required": ["activeTurnNotSteerable"], + "title": "ActiveTurnNotSteerableCodexErrorInfo", + "type": "object" + } + ] + }, + "CollabAgentState": { + "properties": { + "message": { + "type": ["string", "null"] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } + }, + "required": ["status"], + "type": "object" + }, + "CollabAgentStatus": { + "enum": [ + "pendingInit", + "running", + "interrupted", + "completed", + "errored", + "shutdown", + "notFound" + ], + "type": "string" + }, + "CollabAgentTool": { + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents" + ], + "type": "string" + }, + "CollabAgentToolCallStatus": { + "enum": ["inProgress", "completed", "failed", "interrupted"], + "type": "string" + }, + "CommandAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["read"], + "title": "ReadCommandActionType", + "type": "string" + } + }, + "required": ["command", "name", "path", "type"], + "title": "ReadCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["listFiles"], + "title": "ListFilesCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "ListFilesCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": ["string", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "SearchCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "type": { + "enum": ["unknown"], + "title": "UnknownCommandActionType", + "type": "string" + } + }, + "required": ["command", "type"], + "title": "UnknownCommandAction", + "type": "object" + } + ] + }, + "CommandExecutionSource": { + "enum": ["agent", "userShell", "unifiedExecStartup", "unifiedExecInteraction"], + "type": "string" + }, + "CommandExecutionStatus": { + "enum": ["inProgress", "completed", "failed", "declined"], + "type": "string" + }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": ["inputText"], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": ["inputImage"], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["imageUrl", "type"], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audioUrl": { + "type": "string" + }, + "type": { + "enum": ["inputAudio"], + "title": "InputAudioDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": ["audioUrl", "type"], + "title": "InputAudioDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": ["inProgress", "completed", "failed"], + "type": "string" + }, + "FileUpdateChange": { + "properties": { + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } + }, + "required": ["diff", "kind", "path"], + "type": "object" + }, + "HookPromptFragment": { + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["hookRunId", "text"], + "type": "object" + }, + "ImageDetail": { + "enum": ["auto", "low", "high", "original"], + "type": "string" + }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": ["integer", "null"] + }, + "type": { + "enum": ["usageLimitExceeded"], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": ["limitId", "type"], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, + "LegacyAppPathString": { + "type": "string" + }, + "McpToolCallAppContext": { + "properties": { + "actionName": { + "type": ["string", "null"] + }, + "appName": { + "type": ["string", "null"] + }, + "connectorId": { + "type": "string" + }, + "linkId": { + "type": ["string", "null"] + }, + "resourceUri": { + "type": ["string", "null"] + } + }, + "required": ["connectorId"], + "type": "object" + }, + "McpToolCallError": { + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, + "McpToolCallResult": { + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "structuredContent": true + }, + "required": ["content"], + "type": "object" + }, + "McpToolCallStatus": { + "enum": ["inProgress", "completed", "failed"], + "type": "string" + }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "threadIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["entries", "threadIds"], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": ["lineEnd", "lineStart", "note", "path"], + "type": "object" + }, + "MessagePhase": { + "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", + "oneOf": [ + { + "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", + "enum": ["commentary"], + "type": "string" + }, + { + "description": "The assistant's terminal answer text for the current turn.", + "enum": ["final_answer"], + "type": "string" + } + ] + }, + "NonSteerableTurnKind": { + "enum": ["review", "compact"], + "type": "string" + }, + "PatchApplyStatus": { + "enum": ["inProgress", "completed", "failed", "declined"], + "type": "string" + }, + "PatchChangeKind": { + "oneOf": [ + { + "properties": { + "type": { + "enum": ["add"], + "title": "AddPatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "AddPatchChangeKind", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["delete"], + "title": "DeletePatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "DeletePatchChangeKind", + "type": "object" + }, + { + "properties": { + "move_path": { + "type": ["string", "null"] + }, + "type": { + "enum": ["update"], + "title": "UpdatePatchChangeKindType", + "type": "string" + } + }, + "required": ["type"], + "title": "UpdatePatchChangeKind", + "type": "object" + } + ] + }, + "ReasoningEffort": { + "description": "A non-empty reasoning effort value advertised by the model.", + "minLength": 1, + "type": "string" + }, + "SubAgentActivityKind": { + "enum": ["started", "interacted", "interrupted", "completed"], + "type": "string" + }, + "TextElement": { + "properties": { + "byteRange": { + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ], + "description": "Byte range in the parent `text` buffer that this element occupies." + }, + "placeholder": { + "description": "Optional human-readable placeholder for the element, displayed in the UI.", + "type": ["string", "null"] + } + }, + "required": ["byteRange"], + "type": "object" + }, + "ThreadItem": { + "oneOf": [ + { + "properties": { + "clientId": { + "type": ["string", "null"] + }, + "content": { + "items": { + "$ref": "#/definitions/UserInput" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["userMessage"], + "title": "UserMessageThreadItemType", + "type": "string" + } + }, + "required": ["content", "id", "type"], + "title": "UserMessageThreadItem", + "type": "object" + }, + { + "properties": { + "fragments": { + "items": { + "$ref": "#/definitions/HookPromptFragment" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["hookPrompt"], + "title": "HookPromptThreadItemType", + "type": "string" + } + }, + "required": ["fragments", "id", "type"], + "title": "HookPromptThreadItem", + "type": "object" + }, + { + "properties": { + "delivery": { + "anyOf": [ + { + "$ref": "#/definitions/AgentMessageDelivery" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ], + "default": null + }, + "text": { + "type": "string" + }, + "type": { + "enum": ["agentMessage"], + "title": "AgentMessageThreadItemType", + "type": "string" + } + }, + "required": ["id", "text", "type"], + "title": "AgentMessageThreadItem", + "type": "object" + }, + { + "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "enum": ["plan"], + "title": "PlanThreadItemType", + "type": "string" + } + }, + "required": ["id", "text", "type"], + "title": "PlanThreadItem", + "type": "object" + }, + { + "properties": { + "content": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "enum": ["reasoning"], + "title": "ReasoningThreadItemType", + "type": "string" + } + }, + "required": ["id", "type"], + "title": "ReasoningThreadItem", + "type": "object" + }, + { + "properties": { + "aggregatedOutput": { + "description": "The command's output, aggregated from stdout and stderr.", + "type": ["string", "null"] + }, + "command": { + "description": "The command to be executed.", + "type": "string" + }, + "commandActions": { + "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", + "items": { + "$ref": "#/definitions/CommandAction" + }, + "type": "array" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + } + ], + "description": "The command's working directory." + }, + "durationMs": { + "description": "The duration of the command execution in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "exitCode": { + "description": "The command's exit code.", + "format": "int32", + "type": ["integer", "null"] + }, + "id": { + "type": "string" + }, + "pluginId": { + "default": null, + "description": "Trusted first-party plugin id when this command resolves to one plugin script.", + "type": ["string", "null"] + }, + "processId": { + "description": "Identifier for the underlying PTY process (when available).", + "type": ["string", "null"] + }, + "scriptPath": { + "default": null, + "description": "Safe plugin-relative path when this command resolves to one plugin script.", + "type": ["string", "null"] + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ], + "default": "agent" + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" + }, + "type": { + "enum": ["commandExecution"], + "title": "CommandExecutionThreadItemType", + "type": "string" + } + }, + "required": ["command", "commandActions", "cwd", "id", "status", "type"], + "title": "CommandExecutionThreadItem", + "type": "object" + }, + { + "properties": { + "changes": { + "items": { + "$ref": "#/definitions/FileUpdateChange" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, + "type": { + "enum": ["fileChange"], + "title": "FileChangeThreadItemType", + "type": "string" + } + }, + "required": ["changes", "id", "status", "type"], + "title": "FileChangeThreadItem", + "type": "object" + }, + { + "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, + "arguments": true, + "durationMs": { + "description": "The duration of the MCP tool call in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", + "type": ["string", "null"] + }, + "pluginId": { + "type": ["string", "null"] + }, + "readOnlyHint": { + "type": ["boolean", "null"] + }, + "result": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" + }, + "type": { + "enum": ["mcpToolCall"], + "title": "McpToolCallThreadItemType", + "type": "string" + } + }, + "required": ["arguments", "id", "server", "status", "tool", "type"], + "title": "McpToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": ["array", "null"] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": ["integer", "null"] + }, + "id": { + "type": "string" + }, + "namespace": { + "type": ["string", "null"] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": ["boolean", "null"] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": ["dynamicToolCall"], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": ["arguments", "id", "status", "tool", "type"], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentsStates": { + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + }, + "description": "Last known status of the target agents, when available.", + "type": "object" + }, + "id": { + "description": "Unique identifier for this collab tool call.", + "type": "string" + }, + "model": { + "description": "Model requested for the spawned agent, when applicable.", + "type": ["string", "null"] + }, + "prompt": { + "description": "Prompt text sent as part of the collab tool call, when available.", + "type": ["string", "null"] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Reasoning effort requested for the spawned agent, when applicable." + }, + "receiverThreadIds": { + "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", + "items": { + "type": "string" + }, + "type": "array" + }, + "senderThreadId": { + "description": "Thread ID of the agent issuing the collab request.", + "type": "string" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ], + "description": "Current status of the collab tool call." + }, + "tool": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ], + "description": "Name of the collab tool that was invoked." + }, + "type": { + "enum": ["collabAgentToolCall"], + "title": "CollabAgentToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "agentsStates", + "id", + "receiverThreadIds", + "senderThreadId", + "status", + "tool", + "type" + ], + "title": "CollabAgentToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentPath": { + "type": "string" + }, + "agentThreadId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/SubAgentActivityKind" + }, + "type": { + "enum": ["subAgentActivity"], + "title": "SubAgentActivityThreadItemType", + "type": "string" + } + }, + "required": ["agentPath", "agentThreadId", "id", "kind", "type"], + "title": "SubAgentActivityThreadItem", + "type": "object" + }, + { + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "results": { + "default": null, + "description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + "items": true, + "type": ["array", "null"] + }, + "type": { + "enum": ["webSearch"], + "title": "WebSearchThreadItemType", + "type": "string" + } + }, + "required": ["id", "query", "type"], + "title": "WebSearchThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": ["imageView"], + "title": "ImageViewThreadItemType", + "type": "string" + } + }, + "required": ["id", "path", "type"], + "title": "ImageViewThreadItem", + "type": "object" + }, + { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", + "properties": { + "durationMs": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "id": { + "type": "string" + }, + "type": { + "enum": ["sleep"], + "title": "SleepThreadItemType", + "type": "string" + } + }, + "required": ["durationMs", "id", "type"], + "title": "SleepThreadItem", + "type": "object" + }, + { + "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": ["string", "null"] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" + }, + "transparentBackground": { + "default": null, + "type": ["boolean", "null"] + }, + "type": { + "enum": ["imageGeneration"], + "title": "ImageGenerationThreadItemType", + "type": "string" + } + }, + "required": ["id", "result", "status", "type"], + "title": "ImageGenerationThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": ["enteredReviewMode"], + "title": "EnteredReviewModeThreadItemType", + "type": "string" + } + }, + "required": ["id", "review", "type"], + "title": "EnteredReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": ["exitedReviewMode"], + "title": "ExitedReviewModeThreadItemType", + "type": "string" + } + }, + "required": ["id", "review", "type"], + "title": "ExitedReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "type": { + "enum": ["contextCompaction"], + "title": "ContextCompactionThreadItemType", + "type": "string" + } + }, + "required": ["id", "type"], + "title": "ContextCompactionThreadItem", + "type": "object" + } + ] + }, + "Turn": { + "properties": { + "completedAt": { + "description": "Unix timestamp (in seconds) when the turn completed.", + "format": "int64", + "type": ["integer", "null"] + }, + "durationMs": { + "description": "Duration between turn start and completion in milliseconds, if known.", + "format": "int64", + "type": ["integer", "null"] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ], + "description": "Only populated when the Turn's status is failed." + }, + "id": { + "description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + "type": "string" + }, + "items": { + "description": "Thread items currently included in this turn payload.", + "items": { + "$ref": "#/definitions/ThreadItem" + }, + "type": "array" + }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, + "startedAt": { + "description": "Unix timestamp (in seconds) when the turn started.", + "format": "int64", + "type": ["integer", "null"] + }, + "status": { + "$ref": "#/definitions/TurnStatus" + } + }, + "required": ["id", "items", "status"], + "type": "object" + }, + "TurnError": { + "properties": { + "additionalDetails": { + "default": null, + "type": ["string", "null"] + }, + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": ["notLoaded"], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": ["summary"], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": ["full"], + "type": "string" + } + ] + }, + "TurnStatus": { + "enum": ["completed", "interrupted", "failed", "inProgress"], + "type": "string" + }, + "UserInput": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "text_elements": { + "default": [], + "description": "UI-defined spans within `text` used to render or persist special elements.", + "items": { + "$ref": "#/definitions/TextElement" + }, + "type": "array" + }, + "type": { + "enum": ["text"], + "title": "TextUserInputType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "TextUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "type": { + "enum": ["image"], + "title": "ImageUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["type", "url"], + "title": "ImageUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["localImage"], + "title": "LocalImageUserInputType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "LocalImageUserInput", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["audio"], + "title": "AudioUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["type", "url"], + "title": "AudioUserInput", + "type": "object" + }, + { + "properties": { + "path": { + "type": "string" + }, + "type": { + "enum": ["localAudio"], + "title": "LocalAudioUserInputType", + "type": "string" + } + }, + "required": ["path", "type"], + "title": "LocalAudioUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["skill"], + "title": "SkillUserInputType", + "type": "string" + } + }, + "required": ["name", "path", "type"], + "title": "SkillUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": ["mention"], + "title": "MentionUserInputType", + "type": "string" + } + }, + "required": ["name", "path", "type"], + "title": "MentionUserInput", + "type": "object" + } + ] + }, + "WebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "query": { + "type": ["string", "null"] + }, + "type": { + "enum": ["search"], + "title": "SearchWebSearchActionType", + "type": "string" + } + }, + "required": ["type"], + "title": "SearchWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["openPage"], + "title": "OpenPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": ["string", "null"] + } + }, + "required": ["type"], + "title": "OpenPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": ["string", "null"] + }, + "type": { + "enum": ["findInPage"], + "title": "FindInPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": ["string", "null"] + } + }, + "required": ["type"], + "title": "FindInPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": ["other"], + "title": "OtherWebSearchActionType", + "type": "string" + } + }, + "required": ["type"], + "title": "OtherWebSearchAction", + "type": "object" + } + ] + } + }, + "properties": { + "turn": { + "$ref": "#/definitions/Turn" + } + }, + "required": ["turn"], + "title": "TurnStartResponse", + "type": "object" +} diff --git a/src/runtimes/openai/generated/AbsolutePathBuf.ts b/src/runtimes/openai/generated/AbsolutePathBuf.ts new file mode 100644 index 0000000..dc1cde1 --- /dev/null +++ b/src/runtimes/openai/generated/AbsolutePathBuf.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * A path that is guaranteed to be absolute and normalized (though it is not + * guaranteed to be canonicalized or exist on the filesystem). + * + * IMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set + * using [AbsolutePathBufGuard::new]. If no base path is set, the + * deserialization will fail unless the path being deserialized is already + * absolute. + */ +export type AbsolutePathBuf = string; diff --git a/src/runtimes/openai/generated/AgentMessageInputContent.ts b/src/runtimes/openai/generated/AgentMessageInputContent.ts new file mode 100644 index 0000000..400783e --- /dev/null +++ b/src/runtimes/openai/generated/AgentMessageInputContent.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AgentMessageInputContent = + | { type: "input_text"; text: string } + | { type: "encrypted_content"; encrypted_content: string }; diff --git a/src/runtimes/openai/generated/AgentPath.ts b/src/runtimes/openai/generated/AgentPath.ts new file mode 100644 index 0000000..6e55ce6 --- /dev/null +++ b/src/runtimes/openai/generated/AgentPath.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AgentPath = string; diff --git a/src/runtimes/openai/generated/ApplyPatchApprovalParams.ts b/src/runtimes/openai/generated/ApplyPatchApprovalParams.ts new file mode 100644 index 0000000..ab04f9f --- /dev/null +++ b/src/runtimes/openai/generated/ApplyPatchApprovalParams.ts @@ -0,0 +1,24 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FileChange } from "./FileChange"; +import type { ThreadId } from "./ThreadId"; + +export type ApplyPatchApprovalParams = { + conversationId: ThreadId; + /** + * Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent] + * and [codex_protocol::protocol::PatchApplyEndEvent]. + */ + callId: string; + fileChanges: { [key in string]?: FileChange }; + /** + * Optional explanatory reason (e.g. request for extra write access). + */ + reason: string | null; + /** + * When set, the agent is asking the user to allow writes under this root + * for the remainder of the session (unclear if this is honored today). + */ + grantRoot: string | null; +}; diff --git a/src/runtimes/openai/generated/ApplyPatchApprovalResponse.ts b/src/runtimes/openai/generated/ApplyPatchApprovalResponse.ts new file mode 100644 index 0000000..f8ebead --- /dev/null +++ b/src/runtimes/openai/generated/ApplyPatchApprovalResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewDecision } from "./ReviewDecision"; + +export type ApplyPatchApprovalResponse = { decision: ReviewDecision }; diff --git a/src/runtimes/openai/generated/AuthMode.ts b/src/runtimes/openai/generated/AuthMode.ts new file mode 100644 index 0000000..820d826 --- /dev/null +++ b/src/runtimes/openai/generated/AuthMode.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Authentication mode for OpenAI-backed providers. + */ +export type AuthMode = + | "apikey" + | "chatgpt" + | "chatgptAuthTokens" + | "headers" + | "agentIdentity" + | "personalAccessToken" + | "bedrockApiKey" + | "bedrockAccessKeys"; diff --git a/src/runtimes/openai/generated/AutoCompactTokenLimitScope.ts b/src/runtimes/openai/generated/AutoCompactTokenLimitScope.ts new file mode 100644 index 0000000..3d9a56c --- /dev/null +++ b/src/runtimes/openai/generated/AutoCompactTokenLimitScope.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Selects which part of the active context is charged against + * `model_auto_compact_token_limit`. + */ +export type AutoCompactTokenLimitScope = "total" | "body_after_prefix"; diff --git a/src/runtimes/openai/generated/ClientInfo.ts b/src/runtimes/openai/generated/ClientInfo.ts new file mode 100644 index 0000000..b3871d6 --- /dev/null +++ b/src/runtimes/openai/generated/ClientInfo.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ClientInfo = { name: string; title: string | null; version: string }; diff --git a/src/runtimes/openai/generated/ClientNotification.ts b/src/runtimes/openai/generated/ClientNotification.ts new file mode 100644 index 0000000..814c9de --- /dev/null +++ b/src/runtimes/openai/generated/ClientNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ClientNotification = { method: "initialized" }; diff --git a/src/runtimes/openai/generated/ClientRequest.ts b/src/runtimes/openai/generated/ClientRequest.ts new file mode 100644 index 0000000..2da145b --- /dev/null +++ b/src/runtimes/openai/generated/ClientRequest.ts @@ -0,0 +1,392 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams"; +import type { FuzzyFileSearchSessionStartParams } from "./FuzzyFileSearchSessionStartParams"; +import type { FuzzyFileSearchSessionStopParams } from "./FuzzyFileSearchSessionStopParams"; +import type { FuzzyFileSearchSessionUpdateParams } from "./FuzzyFileSearchSessionUpdateParams"; +import type { GetAuthStatusParams } from "./GetAuthStatusParams"; +import type { GetConversationSummaryParams } from "./GetConversationSummaryParams"; +import type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams"; +import type { InitializeParams } from "./InitializeParams"; +import type { RequestId } from "./RequestId"; +import type { AppsInstalledParams } from "./v2/AppsInstalledParams"; +import type { AppsListParams } from "./v2/AppsListParams"; +import type { AppsReadParams } from "./v2/AppsReadParams"; +import type { BedrockDiscoverParams } from "./v2/BedrockDiscoverParams"; +import type { BedrockSetupParams } from "./v2/BedrockSetupParams"; +import type { CancelLoginAccountParams } from "./v2/CancelLoginAccountParams"; +import type { CollaborationModeListParams } from "./v2/CollaborationModeListParams"; +import type { CommandExecParams } from "./v2/CommandExecParams"; +import type { CommandExecResizeParams } from "./v2/CommandExecResizeParams"; +import type { CommandExecTerminateParams } from "./v2/CommandExecTerminateParams"; +import type { CommandExecWriteParams } from "./v2/CommandExecWriteParams"; +import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams"; +import type { ConfigReadParams } from "./v2/ConfigReadParams"; +import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams"; +import type { ConsumeAccountRateLimitResetCreditParams } from "./v2/ConsumeAccountRateLimitResetCreditParams"; +import type { EnvironmentAddParams } from "./v2/EnvironmentAddParams"; +import type { EnvironmentInfoParams } from "./v2/EnvironmentInfoParams"; +import type { EnvironmentStatusParams } from "./v2/EnvironmentStatusParams"; +import type { ExperimentalFeatureEnablementSetParams } from "./v2/ExperimentalFeatureEnablementSetParams"; +import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureListParams"; +import type { ExternalAgentConfigDetectParams } from "./v2/ExternalAgentConfigDetectParams"; +import type { ExternalAgentConfigImportHistoryRecordParams } from "./v2/ExternalAgentConfigImportHistoryRecordParams"; +import type { ExternalAgentConfigImportParams } from "./v2/ExternalAgentConfigImportParams"; +import type { FeedbackUploadParams } from "./v2/FeedbackUploadParams"; +import type { FsCopyParams } from "./v2/FsCopyParams"; +import type { FsCreateDirectoryParams } from "./v2/FsCreateDirectoryParams"; +import type { FsGetMetadataParams } from "./v2/FsGetMetadataParams"; +import type { FsReadDirectoryParams } from "./v2/FsReadDirectoryParams"; +import type { FsReadFileParams } from "./v2/FsReadFileParams"; +import type { FsRemoveParams } from "./v2/FsRemoveParams"; +import type { FsUnwatchParams } from "./v2/FsUnwatchParams"; +import type { FsWatchParams } from "./v2/FsWatchParams"; +import type { FsWriteFileParams } from "./v2/FsWriteFileParams"; +import type { GetAccountParams } from "./v2/GetAccountParams"; +import type { GetAccountTokenUsageParams } from "./v2/GetAccountTokenUsageParams"; +import type { HooksListParams } from "./v2/HooksListParams"; +import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; +import type { LoginAccountParams } from "./v2/LoginAccountParams"; +import type { MarketplaceAddParams } from "./v2/MarketplaceAddParams"; +import type { MarketplaceRemoveParams } from "./v2/MarketplaceRemoveParams"; +import type { MarketplaceUpgradeParams } from "./v2/MarketplaceUpgradeParams"; +import type { McpResourceReadParams } from "./v2/McpResourceReadParams"; +import type { McpServerEventStreamStartParams } from "./v2/McpServerEventStreamStartParams"; +import type { McpServerEventStreamStopParams } from "./v2/McpServerEventStreamStopParams"; +import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; +import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams"; +import type { MockExperimentalMethodParams } from "./v2/MockExperimentalMethodParams"; +import type { ModelListParams } from "./v2/ModelListParams"; +import type { ModelProviderCapabilitiesReadParams } from "./v2/ModelProviderCapabilitiesReadParams"; +import type { PermissionProfileListParams } from "./v2/PermissionProfileListParams"; +import type { PluginInstallParams } from "./v2/PluginInstallParams"; +import type { PluginInstalledParams } from "./v2/PluginInstalledParams"; +import type { PluginListParams } from "./v2/PluginListParams"; +import type { PluginReadParams } from "./v2/PluginReadParams"; +import type { PluginSearchParams } from "./v2/PluginSearchParams"; +import type { PluginShareCheckoutParams } from "./v2/PluginShareCheckoutParams"; +import type { PluginShareDeleteParams } from "./v2/PluginShareDeleteParams"; +import type { PluginShareListParams } from "./v2/PluginShareListParams"; +import type { PluginShareSaveParams } from "./v2/PluginShareSaveParams"; +import type { PluginShareUpdateTargetsParams } from "./v2/PluginShareUpdateTargetsParams"; +import type { PluginSkillReadParams } from "./v2/PluginSkillReadParams"; +import type { PluginUninstallParams } from "./v2/PluginUninstallParams"; +import type { ProcessKillParams } from "./v2/ProcessKillParams"; +import type { ProcessResizePtyParams } from "./v2/ProcessResizePtyParams"; +import type { ProcessSpawnParams } from "./v2/ProcessSpawnParams"; +import type { ProcessWriteStdinParams } from "./v2/ProcessWriteStdinParams"; +import type { ProjectCreateParams } from "./v2/ProjectCreateParams"; +import type { ProjectDeleteParams } from "./v2/ProjectDeleteParams"; +import type { ProjectImportParams } from "./v2/ProjectImportParams"; +import type { ProjectListParams } from "./v2/ProjectListParams"; +import type { ProjectMoveParams } from "./v2/ProjectMoveParams"; +import type { ProjectReadParams } from "./v2/ProjectReadParams"; +import type { ProjectUpdateParams } from "./v2/ProjectUpdateParams"; +import type { RemoteControlClientsListParams } from "./v2/RemoteControlClientsListParams"; +import type { RemoteControlClientsRevokeParams } from "./v2/RemoteControlClientsRevokeParams"; +import type { RemoteControlDisableParams } from "./v2/RemoteControlDisableParams"; +import type { RemoteControlEnableParams } from "./v2/RemoteControlEnableParams"; +import type { RemoteControlPairingStartParams } from "./v2/RemoteControlPairingStartParams"; +import type { RemoteControlPairingStatusParams } from "./v2/RemoteControlPairingStatusParams"; +import type { ReviewStartParams } from "./v2/ReviewStartParams"; +import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams"; +import type { ServerDiagnosticsParams } from "./v2/ServerDiagnosticsParams"; +import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams"; +import type { SkillsExtraRootsSetParams } from "./v2/SkillsExtraRootsSetParams"; +import type { SkillsListParams } from "./v2/SkillsListParams"; +import type { ThreadApproveGuardianDeniedActionParams } from "./v2/ThreadApproveGuardianDeniedActionParams"; +import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams"; +import type { ThreadBackgroundTerminalsCleanParams } from "./v2/ThreadBackgroundTerminalsCleanParams"; +import type { ThreadBackgroundTerminalsListParams } from "./v2/ThreadBackgroundTerminalsListParams"; +import type { ThreadBackgroundTerminalsTerminateParams } from "./v2/ThreadBackgroundTerminalsTerminateParams"; +import type { ThreadCompactStartParams } from "./v2/ThreadCompactStartParams"; +import type { ThreadDecrementElicitationParams } from "./v2/ThreadDecrementElicitationParams"; +import type { ThreadDeleteParams } from "./v2/ThreadDeleteParams"; +import type { ThreadForkParams } from "./v2/ThreadForkParams"; +import type { ThreadGoalClearParams } from "./v2/ThreadGoalClearParams"; +import type { ThreadGoalGetParams } from "./v2/ThreadGoalGetParams"; +import type { ThreadGoalSetParams } from "./v2/ThreadGoalSetParams"; +import type { ThreadIncrementElicitationParams } from "./v2/ThreadIncrementElicitationParams"; +import type { ThreadInjectItemsParams } from "./v2/ThreadInjectItemsParams"; +import type { ThreadItemsListParams } from "./v2/ThreadItemsListParams"; +import type { ThreadListParams } from "./v2/ThreadListParams"; +import type { ThreadLoadedListParams } from "./v2/ThreadLoadedListParams"; +import type { ThreadMemoryModeSetParams } from "./v2/ThreadMemoryModeSetParams"; +import type { ThreadMetadataUpdateParams } from "./v2/ThreadMetadataUpdateParams"; +import type { ThreadQueueAddParams } from "./v2/ThreadQueueAddParams"; +import type { ThreadQueueDeleteParams } from "./v2/ThreadQueueDeleteParams"; +import type { ThreadQueueListParams } from "./v2/ThreadQueueListParams"; +import type { ThreadQueueReorderParams } from "./v2/ThreadQueueReorderParams"; +import type { ThreadQueueStartParams } from "./v2/ThreadQueueStartParams"; +import type { ThreadQueueUpdateParams } from "./v2/ThreadQueueUpdateParams"; +import type { ThreadReadParams } from "./v2/ThreadReadParams"; +import type { ThreadRealtimeAppendAudioParams } from "./v2/ThreadRealtimeAppendAudioParams"; +import type { ThreadRealtimeAppendSpeechParams } from "./v2/ThreadRealtimeAppendSpeechParams"; +import type { ThreadRealtimeAppendTextParams } from "./v2/ThreadRealtimeAppendTextParams"; +import type { ThreadRealtimeListVoicesParams } from "./v2/ThreadRealtimeListVoicesParams"; +import type { ThreadRealtimeStartParams } from "./v2/ThreadRealtimeStartParams"; +import type { ThreadRealtimeStopParams } from "./v2/ThreadRealtimeStopParams"; +import type { ThreadResumeParams } from "./v2/ThreadResumeParams"; +import type { ThreadRevertParams } from "./v2/ThreadRevertParams"; +import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams"; +import type { ThreadSearchOccurrencesParams } from "./v2/ThreadSearchOccurrencesParams"; +import type { ThreadSearchParams } from "./v2/ThreadSearchParams"; +import type { ThreadSectionCreateParams } from "./v2/ThreadSectionCreateParams"; +import type { ThreadSectionDeleteParams } from "./v2/ThreadSectionDeleteParams"; +import type { ThreadSectionListParams } from "./v2/ThreadSectionListParams"; +import type { ThreadSectionMoveParams } from "./v2/ThreadSectionMoveParams"; +import type { ThreadSectionUpdateParams } from "./v2/ThreadSectionUpdateParams"; +import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams"; +import type { ThreadSettingsUpdateParams } from "./v2/ThreadSettingsUpdateParams"; +import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams"; +import type { ThreadStartParams } from "./v2/ThreadStartParams"; +import type { ThreadTimelineListParams } from "./v2/ThreadTimelineListParams"; +import type { ThreadTurnsListParams } from "./v2/ThreadTurnsListParams"; +import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams"; +import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams"; +import type { TurnInterruptParams } from "./v2/TurnInterruptParams"; +import type { TurnStartParams } from "./v2/TurnStartParams"; +import type { TurnSteerParams } from "./v2/TurnSteerParams"; +import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupStartParams"; + +/** + * Request from the client to the server. + */ +export type ClientRequest = + | { method: "initialize"; id: RequestId; params: InitializeParams } + | { method: "server/diagnostics"; id: RequestId; params: ServerDiagnosticsParams } + | { method: "thread/start"; id: RequestId; params: ThreadStartParams } + | { method: "thread/resume"; id: RequestId; params: ThreadResumeParams } + | { method: "thread/fork"; id: RequestId; params: ThreadForkParams } + | { method: "thread/archive"; id: RequestId; params: ThreadArchiveParams } + | { method: "thread/delete"; id: RequestId; params: ThreadDeleteParams } + | { method: "thread/unsubscribe"; id: RequestId; params: ThreadUnsubscribeParams } + | { + method: "thread/increment_elicitation"; + id: RequestId; + params: ThreadIncrementElicitationParams; + } + | { + method: "thread/decrement_elicitation"; + id: RequestId; + params: ThreadDecrementElicitationParams; + } + | { method: "thread/name/set"; id: RequestId; params: ThreadSetNameParams } + | { method: "thread/goal/set"; id: RequestId; params: ThreadGoalSetParams } + | { method: "thread/goal/get"; id: RequestId; params: ThreadGoalGetParams } + | { method: "thread/goal/clear"; id: RequestId; params: ThreadGoalClearParams } + | { method: "thread/queue/add"; id: RequestId; params: ThreadQueueAddParams } + | { method: "thread/queue/list"; id: RequestId; params: ThreadQueueListParams } + | { method: "thread/queue/update"; id: RequestId; params: ThreadQueueUpdateParams } + | { method: "thread/queue/delete"; id: RequestId; params: ThreadQueueDeleteParams } + | { method: "thread/queue/reorder"; id: RequestId; params: ThreadQueueReorderParams } + | { method: "thread/queue/start"; id: RequestId; params: ThreadQueueStartParams } + | { method: "thread/metadata/update"; id: RequestId; params: ThreadMetadataUpdateParams } + | { method: "thread/section/move"; id: RequestId; params: ThreadSectionMoveParams } + | { method: "thread/settings/update"; id: RequestId; params: ThreadSettingsUpdateParams } + | { method: "thread/memoryMode/set"; id: RequestId; params: ThreadMemoryModeSetParams } + | { method: "memory/reset"; id: RequestId; params: undefined } + | { method: "thread/unarchive"; id: RequestId; params: ThreadUnarchiveParams } + | { method: "thread/compact/start"; id: RequestId; params: ThreadCompactStartParams } + | { method: "thread/shellCommand"; id: RequestId; params: ThreadShellCommandParams } + | { + method: "thread/approveGuardianDeniedAction"; + id: RequestId; + params: ThreadApproveGuardianDeniedActionParams; + } + | { + method: "thread/backgroundTerminals/clean"; + id: RequestId; + params: ThreadBackgroundTerminalsCleanParams; + } + | { + method: "thread/backgroundTerminals/list"; + id: RequestId; + params: ThreadBackgroundTerminalsListParams; + } + | { + method: "thread/backgroundTerminals/terminate"; + id: RequestId; + params: ThreadBackgroundTerminalsTerminateParams; + } + | { method: "thread/rollback"; id: RequestId; params: ThreadRollbackParams } + | { method: "thread/revert"; id: RequestId; params: ThreadRevertParams } + | { method: "thread/list"; id: RequestId; params: ThreadListParams } + | { method: "project/list"; id: RequestId; params: ProjectListParams } + | { method: "project/read"; id: RequestId; params: ProjectReadParams } + | { method: "project/create"; id: RequestId; params: ProjectCreateParams } + | { method: "project/import"; id: RequestId; params: ProjectImportParams } + | { method: "project/update"; id: RequestId; params: ProjectUpdateParams } + | { method: "project/move"; id: RequestId; params: ProjectMoveParams } + | { method: "project/delete"; id: RequestId; params: ProjectDeleteParams } + | { method: "threadSection/list"; id: RequestId; params: ThreadSectionListParams } + | { method: "threadSection/create"; id: RequestId; params: ThreadSectionCreateParams } + | { method: "threadSection/update"; id: RequestId; params: ThreadSectionUpdateParams } + | { method: "threadSection/delete"; id: RequestId; params: ThreadSectionDeleteParams } + | { method: "thread/search"; id: RequestId; params: ThreadSearchParams } + | { method: "thread/searchOccurrences"; id: RequestId; params: ThreadSearchOccurrencesParams } + | { method: "thread/loaded/list"; id: RequestId; params: ThreadLoadedListParams } + | { method: "thread/read"; id: RequestId; params: ThreadReadParams } + | { method: "thread/turns/list"; id: RequestId; params: ThreadTurnsListParams } + | { method: "thread/items/list"; id: RequestId; params: ThreadItemsListParams } + | { method: "thread/inject_items"; id: RequestId; params: ThreadInjectItemsParams } + | { method: "skills/list"; id: RequestId; params: SkillsListParams } + | { method: "skills/extraRoots/set"; id: RequestId; params: SkillsExtraRootsSetParams } + | { method: "hooks/list"; id: RequestId; params: HooksListParams } + | { method: "marketplace/add"; id: RequestId; params: MarketplaceAddParams } + | { method: "marketplace/remove"; id: RequestId; params: MarketplaceRemoveParams } + | { method: "marketplace/upgrade"; id: RequestId; params: MarketplaceUpgradeParams } + | { method: "plugin/list"; id: RequestId; params: PluginListParams } + | { method: "plugin/search"; id: RequestId; params: PluginSearchParams } + | { method: "plugin/installed"; id: RequestId; params: PluginInstalledParams } + | { method: "plugin/read"; id: RequestId; params: PluginReadParams } + | { method: "plugin/skill/read"; id: RequestId; params: PluginSkillReadParams } + | { method: "plugin/share/save"; id: RequestId; params: PluginShareSaveParams } + | { method: "plugin/share/updateTargets"; id: RequestId; params: PluginShareUpdateTargetsParams } + | { method: "plugin/share/list"; id: RequestId; params: PluginShareListParams } + | { method: "plugin/share/checkout"; id: RequestId; params: PluginShareCheckoutParams } + | { method: "plugin/share/delete"; id: RequestId; params: PluginShareDeleteParams } + | { method: "app/read"; id: RequestId; params: AppsReadParams } + | { method: "app/list"; id: RequestId; params: AppsListParams } + | { method: "app/installed"; id: RequestId; params: AppsInstalledParams } + | { method: "fs/readFile"; id: RequestId; params: FsReadFileParams } + | { method: "fs/writeFile"; id: RequestId; params: FsWriteFileParams } + | { method: "fs/createDirectory"; id: RequestId; params: FsCreateDirectoryParams } + | { method: "fs/getMetadata"; id: RequestId; params: FsGetMetadataParams } + | { method: "fs/readDirectory"; id: RequestId; params: FsReadDirectoryParams } + | { method: "fs/remove"; id: RequestId; params: FsRemoveParams } + | { method: "fs/copy"; id: RequestId; params: FsCopyParams } + | { method: "fs/watch"; id: RequestId; params: FsWatchParams } + | { method: "fs/unwatch"; id: RequestId; params: FsUnwatchParams } + | { method: "skills/config/write"; id: RequestId; params: SkillsConfigWriteParams } + | { method: "plugin/install"; id: RequestId; params: PluginInstallParams } + | { method: "plugin/uninstall"; id: RequestId; params: PluginUninstallParams } + | { method: "turn/start"; id: RequestId; params: TurnStartParams } + | { method: "turn/steer"; id: RequestId; params: TurnSteerParams } + | { method: "turn/interrupt"; id: RequestId; params: TurnInterruptParams } + | { method: "thread/realtime/start"; id: RequestId; params: ThreadRealtimeStartParams } + | { + method: "thread/realtime/appendAudio"; + id: RequestId; + params: ThreadRealtimeAppendAudioParams; + } + | { method: "thread/realtime/appendText"; id: RequestId; params: ThreadRealtimeAppendTextParams } + | { + method: "thread/realtime/appendSpeech"; + id: RequestId; + params: ThreadRealtimeAppendSpeechParams; + } + | { method: "thread/realtime/stop"; id: RequestId; params: ThreadRealtimeStopParams } + | { method: "thread/timeline/list"; id: RequestId; params: ThreadTimelineListParams } + | { method: "thread/realtime/listVoices"; id: RequestId; params: ThreadRealtimeListVoicesParams } + | { method: "review/start"; id: RequestId; params: ReviewStartParams } + | { method: "model/list"; id: RequestId; params: ModelListParams } + | { + method: "modelProvider/capabilities/read"; + id: RequestId; + params: ModelProviderCapabilitiesReadParams; + } + | { method: "experimentalFeature/list"; id: RequestId; params: ExperimentalFeatureListParams } + | { method: "permissionProfile/list"; id: RequestId; params: PermissionProfileListParams } + | { + method: "experimentalFeature/enablement/set"; + id: RequestId; + params: ExperimentalFeatureEnablementSetParams; + } + | { method: "remoteControl/enable"; id: RequestId; params: RemoteControlEnableParams | null } + | { method: "remoteControl/disable"; id: RequestId; params: RemoteControlDisableParams | null } + | { method: "remoteControl/status/read"; id: RequestId; params: undefined } + | { + method: "remoteControl/pairing/start"; + id: RequestId; + params: RemoteControlPairingStartParams; + } + | { + method: "remoteControl/pairing/status"; + id: RequestId; + params: RemoteControlPairingStatusParams; + } + | { method: "remoteControl/client/list"; id: RequestId; params: RemoteControlClientsListParams } + | { + method: "remoteControl/client/revoke"; + id: RequestId; + params: RemoteControlClientsRevokeParams; + } + | { method: "collaborationMode/list"; id: RequestId; params: CollaborationModeListParams } + | { method: "mock/experimentalMethod"; id: RequestId; params: MockExperimentalMethodParams } + | { method: "environment/add"; id: RequestId; params: EnvironmentAddParams } + | { method: "environment/info"; id: RequestId; params: EnvironmentInfoParams } + | { method: "environment/status"; id: RequestId; params: EnvironmentStatusParams } + | { method: "mcpServer/oauth/login"; id: RequestId; params: McpServerOauthLoginParams } + | { method: "config/mcpServer/reload"; id: RequestId; params: undefined } + | { method: "mcpServerStatus/list"; id: RequestId; params: ListMcpServerStatusParams } + | { method: "mcpServer/resource/read"; id: RequestId; params: McpResourceReadParams } + | { + method: "mcpServer/event/stream/start"; + id: RequestId; + params: McpServerEventStreamStartParams; + } + | { method: "mcpServer/event/stream/stop"; id: RequestId; params: McpServerEventStreamStopParams } + | { method: "mcpServer/tool/call"; id: RequestId; params: McpServerToolCallParams } + | { method: "windowsSandbox/setupStart"; id: RequestId; params: WindowsSandboxSetupStartParams } + | { method: "windowsSandbox/readiness"; id: RequestId; params: undefined } + | { method: "account/login/start"; id: RequestId; params: LoginAccountParams } + | { method: "account/bedrock/discover"; id: RequestId; params: BedrockDiscoverParams } + | { method: "account/bedrock/setup"; id: RequestId; params: BedrockSetupParams } + | { method: "account/login/cancel"; id: RequestId; params: CancelLoginAccountParams } + | { method: "account/logout"; id: RequestId; params: undefined } + | { method: "account/rateLimits/read"; id: RequestId; params: undefined } + | { + method: "account/rateLimitResetCredit/consume"; + id: RequestId; + params: ConsumeAccountRateLimitResetCreditParams; + } + | { method: "account/usage/read"; id: RequestId; params?: GetAccountTokenUsageParams | undefined } + | { method: "account/workspaceMessages/read"; id: RequestId; params: undefined } + | { + method: "account/sendAddCreditsNudgeEmail"; + id: RequestId; + params: SendAddCreditsNudgeEmailParams; + } + | { method: "feedback/upload"; id: RequestId; params: FeedbackUploadParams } + | { method: "command/exec"; id: RequestId; params: CommandExecParams } + | { method: "command/exec/write"; id: RequestId; params: CommandExecWriteParams } + | { method: "command/exec/terminate"; id: RequestId; params: CommandExecTerminateParams } + | { method: "command/exec/resize"; id: RequestId; params: CommandExecResizeParams } + | { method: "process/spawn"; id: RequestId; params: ProcessSpawnParams } + | { method: "process/writeStdin"; id: RequestId; params: ProcessWriteStdinParams } + | { method: "process/kill"; id: RequestId; params: ProcessKillParams } + | { method: "process/resizePty"; id: RequestId; params: ProcessResizePtyParams } + | { method: "config/read"; id: RequestId; params: ConfigReadParams } + | { method: "externalAgentConfig/detect"; id: RequestId; params: ExternalAgentConfigDetectParams } + | { method: "externalAgentConfig/import"; id: RequestId; params: ExternalAgentConfigImportParams } + | { + method: "externalAgentConfig/import/recordHistory"; + id: RequestId; + params: ExternalAgentConfigImportHistoryRecordParams; + } + | { method: "externalAgentConfig/import/readHistories"; id: RequestId; params: undefined } + | { method: "config/value/write"; id: RequestId; params: ConfigValueWriteParams } + | { method: "config/batchWrite"; id: RequestId; params: ConfigBatchWriteParams } + | { method: "configRequirements/read"; id: RequestId; params: undefined } + | { method: "account/read"; id: RequestId; params: GetAccountParams } + | { method: "getConversationSummary"; id: RequestId; params: GetConversationSummaryParams } + | { method: "gitDiffToRemote"; id: RequestId; params: GitDiffToRemoteParams } + | { method: "getAuthStatus"; id: RequestId; params: GetAuthStatusParams } + | { method: "fuzzyFileSearch"; id: RequestId; params: FuzzyFileSearchParams } + | { + method: "fuzzyFileSearch/sessionStart"; + id: RequestId; + params: FuzzyFileSearchSessionStartParams; + } + | { + method: "fuzzyFileSearch/sessionUpdate"; + id: RequestId; + params: FuzzyFileSearchSessionUpdateParams; + } + | { + method: "fuzzyFileSearch/sessionStop"; + id: RequestId; + params: FuzzyFileSearchSessionStopParams; + }; diff --git a/src/runtimes/openai/generated/CodexResponseHandoffMode.ts b/src/runtimes/openai/generated/CodexResponseHandoffMode.ts new file mode 100644 index 0000000..3eb90da --- /dev/null +++ b/src/runtimes/openai/generated/CodexResponseHandoffMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CodexResponseHandoffMode = "thinking" | "commentary" | "bemTags"; diff --git a/src/runtimes/openai/generated/CollaborationMode.ts b/src/runtimes/openai/generated/CollaborationMode.ts new file mode 100644 index 0000000..c021ade --- /dev/null +++ b/src/runtimes/openai/generated/CollaborationMode.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ModeKind } from "./ModeKind"; +import type { Settings } from "./Settings"; + +/** + * Collaboration mode for a Codex session. + */ +export type CollaborationMode = { mode: ModeKind; settings: Settings }; diff --git a/src/runtimes/openai/generated/ContentItem.ts b/src/runtimes/openai/generated/ContentItem.ts new file mode 100644 index 0000000..03b45e9 --- /dev/null +++ b/src/runtimes/openai/generated/ContentItem.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ImageDetail } from "./ImageDetail"; + +export type ContentItem = + | { type: "input_text"; text: string } + | { type: "input_image"; image_url: string; detail?: ImageDetail } + | { type: "input_audio"; audio_url: string } + | { type: "output_text"; text: string }; diff --git a/src/runtimes/openai/generated/ConversationGitInfo.ts b/src/runtimes/openai/generated/ConversationGitInfo.ts new file mode 100644 index 0000000..8715645 --- /dev/null +++ b/src/runtimes/openai/generated/ConversationGitInfo.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ConversationGitInfo = { + sha: string | null; + branch: string | null; + origin_url: string | null; +}; diff --git a/src/runtimes/openai/generated/ConversationSummary.ts b/src/runtimes/openai/generated/ConversationSummary.ts new file mode 100644 index 0000000..1207397 --- /dev/null +++ b/src/runtimes/openai/generated/ConversationSummary.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConversationGitInfo } from "./ConversationGitInfo"; +import type { SessionSource } from "./SessionSource"; +import type { ThreadId } from "./ThreadId"; + +export type ConversationSummary = { + conversationId: ThreadId; + path: string; + preview: string; + timestamp: string | null; + updatedAt: string | null; + modelProvider: string; + cwd: string; + cliVersion: string; + source: SessionSource; + gitInfo: ConversationGitInfo | null; +}; diff --git a/src/runtimes/openai/generated/ConversationTextRole.ts b/src/runtimes/openai/generated/ConversationTextRole.ts new file mode 100644 index 0000000..a4d574b --- /dev/null +++ b/src/runtimes/openai/generated/ConversationTextRole.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ConversationTextRole = "user" | "developer" | "assistant"; diff --git a/src/runtimes/openai/generated/ExecCommandApprovalParams.ts b/src/runtimes/openai/generated/ExecCommandApprovalParams.ts new file mode 100644 index 0000000..3e88f05 --- /dev/null +++ b/src/runtimes/openai/generated/ExecCommandApprovalParams.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ParsedCommand } from "./ParsedCommand"; +import type { ThreadId } from "./ThreadId"; + +export type ExecCommandApprovalParams = { + conversationId: ThreadId; + /** + * Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent] + * and [codex_protocol::protocol::ExecCommandEndEvent]. + */ + callId: string; + /** + * Identifier for this specific approval callback. + */ + approvalId: string | null; + command: Array; + cwd: string; + reason: string | null; + parsedCmd: Array; +}; diff --git a/src/runtimes/openai/generated/ExecCommandApprovalResponse.ts b/src/runtimes/openai/generated/ExecCommandApprovalResponse.ts new file mode 100644 index 0000000..169dab0 --- /dev/null +++ b/src/runtimes/openai/generated/ExecCommandApprovalResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewDecision } from "./ReviewDecision"; + +export type ExecCommandApprovalResponse = { decision: ReviewDecision }; diff --git a/src/runtimes/openai/generated/ExecPolicyAmendment.ts b/src/runtimes/openai/generated/ExecPolicyAmendment.ts new file mode 100644 index 0000000..98e2626 --- /dev/null +++ b/src/runtimes/openai/generated/ExecPolicyAmendment.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Proposed execpolicy change to allow commands starting with this prefix. + * + * The `command` tokens form the prefix that would be added as an execpolicy + * `prefix_rule(..., decision="allow")`, letting the agent bypass approval for + * commands that start with this token sequence. + */ +export type ExecPolicyAmendment = Array; diff --git a/src/runtimes/openai/generated/FileChange.ts b/src/runtimes/openai/generated/FileChange.ts new file mode 100644 index 0000000..da6abdd --- /dev/null +++ b/src/runtimes/openai/generated/FileChange.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FileChange = + | { type: "add"; content: string } + | { type: "delete"; content: string } + | { type: "update"; unified_diff: string; move_path: string | null }; diff --git a/src/runtimes/openai/generated/ForcedLoginMethod.ts b/src/runtimes/openai/generated/ForcedLoginMethod.ts new file mode 100644 index 0000000..c695908 --- /dev/null +++ b/src/runtimes/openai/generated/ForcedLoginMethod.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ForcedLoginMethod = "chatgpt" | "api"; diff --git a/src/runtimes/openai/generated/FunctionCallOutputBody.ts b/src/runtimes/openai/generated/FunctionCallOutputBody.ts new file mode 100644 index 0000000..6bcb7e2 --- /dev/null +++ b/src/runtimes/openai/generated/FunctionCallOutputBody.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem"; + +export type FunctionCallOutputBody = string | Array; diff --git a/src/runtimes/openai/generated/FunctionCallOutputContentItem.ts b/src/runtimes/openai/generated/FunctionCallOutputContentItem.ts new file mode 100644 index 0000000..fc3a7ef --- /dev/null +++ b/src/runtimes/openai/generated/FunctionCallOutputContentItem.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ImageDetail } from "./ImageDetail"; + +/** + * Responses API compatible content items that can be returned by a tool call. + * This is a subset of ContentItem with the types we support as function call outputs. + */ +export type FunctionCallOutputContentItem = + | { type: "input_text"; text: string } + | { type: "input_image"; image_url: string; detail?: ImageDetail } + | { type: "input_audio"; audio_url: string } + | { type: "encrypted_content"; encrypted_content: string }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchMatchType.ts b/src/runtimes/openai/generated/FuzzyFileSearchMatchType.ts new file mode 100644 index 0000000..60e92f9 --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchMatchType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FuzzyFileSearchMatchType = "file" | "directory"; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchParams.ts b/src/runtimes/openai/generated/FuzzyFileSearchParams.ts new file mode 100644 index 0000000..24e397c --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchParams.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FuzzyFileSearchParams = { + query: string; + roots: Array; + cancellationToken: string | null; +}; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchResponse.ts b/src/runtimes/openai/generated/FuzzyFileSearchResponse.ts new file mode 100644 index 0000000..454cae2 --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult"; + +export type FuzzyFileSearchResponse = { files: Array }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchResult.ts b/src/runtimes/openai/generated/FuzzyFileSearchResult.ts new file mode 100644 index 0000000..f27ea28 --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchResult.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType"; + +/** + * Superset of [`codex_file_search::FileMatch`] + */ +export type FuzzyFileSearchResult = { + root: string; + path: string; + match_type: FuzzyFileSearchMatchType; + file_name: string; + score: number; + indices: Array | null; +}; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionCompletedNotification.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionCompletedNotification.ts new file mode 100644 index 0000000..e8f3e39 --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchSessionCompletedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FuzzyFileSearchSessionCompletedNotification = { sessionId: string }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionStartParams.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionStartParams.ts new file mode 100644 index 0000000..f746734 --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchSessionStartParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FuzzyFileSearchSessionStartParams = { sessionId: string; roots: Array }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionStartResponse.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionStartResponse.ts new file mode 100644 index 0000000..cfe1399 --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchSessionStartResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FuzzyFileSearchSessionStartResponse = Record; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionStopParams.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionStopParams.ts new file mode 100644 index 0000000..72da3c8 --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchSessionStopParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FuzzyFileSearchSessionStopParams = { sessionId: string }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionStopResponse.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionStopResponse.ts new file mode 100644 index 0000000..a3500fb --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchSessionStopResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FuzzyFileSearchSessionStopResponse = Record; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateParams.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateParams.ts new file mode 100644 index 0000000..53542ea --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FuzzyFileSearchSessionUpdateParams = { sessionId: string; query: string }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateResponse.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateResponse.ts new file mode 100644 index 0000000..54b8701 --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FuzzyFileSearchSessionUpdateResponse = Record; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdatedNotification.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdatedNotification.ts new file mode 100644 index 0000000..d993091 --- /dev/null +++ b/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdatedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult"; + +export type FuzzyFileSearchSessionUpdatedNotification = { + sessionId: string; + query: string; + files: Array; +}; diff --git a/src/runtimes/openai/generated/GetAuthStatusParams.ts b/src/runtimes/openai/generated/GetAuthStatusParams.ts new file mode 100644 index 0000000..c3f4c1a --- /dev/null +++ b/src/runtimes/openai/generated/GetAuthStatusParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GetAuthStatusParams = { includeToken: boolean | null; refreshToken: boolean | null }; diff --git a/src/runtimes/openai/generated/GetAuthStatusResponse.ts b/src/runtimes/openai/generated/GetAuthStatusResponse.ts new file mode 100644 index 0000000..bad0238 --- /dev/null +++ b/src/runtimes/openai/generated/GetAuthStatusResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AuthMode } from "./AuthMode"; + +export type GetAuthStatusResponse = { + authMethod: AuthMode | null; + authToken: string | null; + requiresOpenaiAuth: boolean | null; +}; diff --git a/src/runtimes/openai/generated/GetConversationSummaryParams.ts b/src/runtimes/openai/generated/GetConversationSummaryParams.ts new file mode 100644 index 0000000..b4d1772 --- /dev/null +++ b/src/runtimes/openai/generated/GetConversationSummaryParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadId } from "./ThreadId"; + +export type GetConversationSummaryParams = { rolloutPath: string } | { conversationId: ThreadId }; diff --git a/src/runtimes/openai/generated/GetConversationSummaryResponse.ts b/src/runtimes/openai/generated/GetConversationSummaryResponse.ts new file mode 100644 index 0000000..a4848ce --- /dev/null +++ b/src/runtimes/openai/generated/GetConversationSummaryResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConversationSummary } from "./ConversationSummary"; + +export type GetConversationSummaryResponse = { summary: ConversationSummary }; diff --git a/src/runtimes/openai/generated/GitDiffToRemoteParams.ts b/src/runtimes/openai/generated/GitDiffToRemoteParams.ts new file mode 100644 index 0000000..31ff4f9 --- /dev/null +++ b/src/runtimes/openai/generated/GitDiffToRemoteParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GitDiffToRemoteParams = { cwd: string }; diff --git a/src/runtimes/openai/generated/GitDiffToRemoteResponse.ts b/src/runtimes/openai/generated/GitDiffToRemoteResponse.ts new file mode 100644 index 0000000..b3d4d7f --- /dev/null +++ b/src/runtimes/openai/generated/GitDiffToRemoteResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { GitSha } from "./GitSha"; + +export type GitDiffToRemoteResponse = { sha: GitSha; diff: string }; diff --git a/src/runtimes/openai/generated/GitSha.ts b/src/runtimes/openai/generated/GitSha.ts new file mode 100644 index 0000000..701b75a --- /dev/null +++ b/src/runtimes/openai/generated/GitSha.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GitSha = string; diff --git a/src/runtimes/openai/generated/ImageDetail.ts b/src/runtimes/openai/generated/ImageDetail.ts new file mode 100644 index 0000000..a48f07c --- /dev/null +++ b/src/runtimes/openai/generated/ImageDetail.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ImageDetail = "auto" | "low" | "high" | "original"; diff --git a/src/runtimes/openai/generated/ImageGenerationFailure.ts b/src/runtimes/openai/generated/ImageGenerationFailure.ts new file mode 100644 index 0000000..19e00d0 --- /dev/null +++ b/src/runtimes/openai/generated/ImageGenerationFailure.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ImageGenerationFailure = { + type: "usageLimitExceeded"; + limitId: string; + resetsAt: number | null; +}; diff --git a/src/runtimes/openai/generated/ImageGenerationItem.ts b/src/runtimes/openai/generated/ImageGenerationItem.ts new file mode 100644 index 0000000..26b36c5 --- /dev/null +++ b/src/runtimes/openai/generated/ImageGenerationItem.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "./AbsolutePathBuf"; +import type { ImageGenerationFailure } from "./ImageGenerationFailure"; + +export type ImageGenerationItem = { + id: string; + status: string; + revisedPrompt: string | null; + result: string; + transparentBackground?: boolean; + failure: ImageGenerationFailure | null; + savedPath?: AbsolutePathBuf; +}; diff --git a/src/runtimes/openai/generated/InitializeCapabilities.ts b/src/runtimes/openai/generated/InitializeCapabilities.ts new file mode 100644 index 0000000..8639419 --- /dev/null +++ b/src/runtimes/openai/generated/InitializeCapabilities.ts @@ -0,0 +1,33 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; + +/** + * Client-declared capabilities negotiated during initialize. + */ +export type InitializeCapabilities = { + /** + * Opt into receiving experimental API methods and fields. + */ + experimentalApi: boolean; + /** + * Opt into `attestation/generate` requests for upstream `x-oai-attestation`. + */ + requestAttestation: boolean; + /** + * Legacy opt-in for the `openai/form` MCP extension. + * + * New clients should declare `openai/form` in [`Self::extensions`]. + */ + mcpServerOpenaiFormElicitation?: boolean; + /** + * Exact notification method names that should be suppressed for this + * connection (for example `thread/started`). + */ + optOutNotificationMethods?: Array | null; + /** + * MCP extension settings declared by the app-server client. + */ + extensions?: { [key in string]?: JsonValue } | null; +}; diff --git a/src/runtimes/openai/generated/InitializeParams.ts b/src/runtimes/openai/generated/InitializeParams.ts new file mode 100644 index 0000000..6502fa4 --- /dev/null +++ b/src/runtimes/openai/generated/InitializeParams.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ClientInfo } from "./ClientInfo"; +import type { InitializeCapabilities } from "./InitializeCapabilities"; + +export type InitializeParams = { + clientInfo: ClientInfo; + capabilities: InitializeCapabilities | null; +}; diff --git a/src/runtimes/openai/generated/InitializeResponse.ts b/src/runtimes/openai/generated/InitializeResponse.ts new file mode 100644 index 0000000..4f4f4f7 --- /dev/null +++ b/src/runtimes/openai/generated/InitializeResponse.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "./AbsolutePathBuf"; + +export type InitializeResponse = { + userAgent: string; + /** + * Absolute path to the server's $CODEX_HOME directory. + */ + codexHome: AbsolutePathBuf; + /** + * Platform family for the running app-server target, for example + * `"unix"` or `"windows"`. + */ + platformFamily: string; + /** + * Operating system for the running app-server target, for example + * `"macos"`, `"linux"`, or `"windows"`. + */ + platformOs: string; +}; diff --git a/src/runtimes/openai/generated/InputModality.ts b/src/runtimes/openai/generated/InputModality.ts new file mode 100644 index 0000000..40d598d --- /dev/null +++ b/src/runtimes/openai/generated/InputModality.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Canonical user-input modality tags advertised by a model. + */ +export type InputModality = "text" | "image" | "audio"; diff --git a/src/runtimes/openai/generated/InternalChatMessageMetadataPassthrough.ts b/src/runtimes/openai/generated/InternalChatMessageMetadataPassthrough.ts new file mode 100644 index 0000000..dff2550 --- /dev/null +++ b/src/runtimes/openai/generated/InternalChatMessageMetadataPassthrough.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Internal Responses API passthrough metadata copied into underlying chat messages. + * + * Responses API strongly types this payload. Do not modify it without first getting API + * approval and making the corresponding Responses API change. + */ +export type InternalChatMessageMetadataPassthrough = { turn_id?: string }; diff --git a/src/runtimes/openai/generated/InternalSessionSource.ts b/src/runtimes/openai/generated/InternalSessionSource.ts new file mode 100644 index 0000000..d847e34 --- /dev/null +++ b/src/runtimes/openai/generated/InternalSessionSource.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type InternalSessionSource = "memory_consolidation" | "guardian"; diff --git a/src/runtimes/openai/generated/LegacyAppPathString.ts b/src/runtimes/openai/generated/LegacyAppPathString.ts new file mode 100644 index 0000000..5c0a1b1 --- /dev/null +++ b/src/runtimes/openai/generated/LegacyAppPathString.ts @@ -0,0 +1,27 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * A UTF-8 path for preserving raw path compatibility at the app-server API + * boundary while Codex migrates to [`PathUri`]. + * + * Supports storing arbitrary strings read from the API and converting to and + * from [`PathUri`] using an explicitly selected native path convention. + * + * When converting from [`PathUri`], "native" refers to the supplied + * [`PathConvention`], which may be foreign to the operating system running + * this process. The inner string is private so path-producing code must use a + * path conversion method instead of bypassing the intended conversion + * boundary. Non-UTF-8 paths are converted to UTF-8 lossily because this API + * value is serialized as a JSON string. + * + * Deserialization and [`Self::from_string`] accept any UTF-8 string without + * interpreting or validating it. Use [`Self::from_string`] when a caller + * already owns legacy app-server path text and needs to preserve its wire + * spelling; use [`Self::from_path`], [`Self::from_abs_path`], or + * [`Self::from_path_uri`] when converting an actual path value. Relative + * path text remains valid until an operation such as [`Self::to_path_uri`] + * requires an absolute path. + */ +export type LegacyAppPathString = string; diff --git a/src/runtimes/openai/generated/LocalShellAction.ts b/src/runtimes/openai/generated/LocalShellAction.ts new file mode 100644 index 0000000..271c098 --- /dev/null +++ b/src/runtimes/openai/generated/LocalShellAction.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LocalShellExecAction } from "./LocalShellExecAction"; + +export type LocalShellAction = { type: "exec" } & LocalShellExecAction; diff --git a/src/runtimes/openai/generated/LocalShellExecAction.ts b/src/runtimes/openai/generated/LocalShellExecAction.ts new file mode 100644 index 0000000..5faf962 --- /dev/null +++ b/src/runtimes/openai/generated/LocalShellExecAction.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type LocalShellExecAction = { + command: Array; + timeout_ms: bigint | null; + working_directory: string | null; + env: { [key in string]?: string } | null; + user: string | null; +}; diff --git a/src/runtimes/openai/generated/LocalShellStatus.ts b/src/runtimes/openai/generated/LocalShellStatus.ts new file mode 100644 index 0000000..00db484 --- /dev/null +++ b/src/runtimes/openai/generated/LocalShellStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type LocalShellStatus = "completed" | "in_progress" | "incomplete"; diff --git a/src/runtimes/openai/generated/McpServerInfo.ts b/src/runtimes/openai/generated/McpServerInfo.ts new file mode 100644 index 0000000..d41876f --- /dev/null +++ b/src/runtimes/openai/generated/McpServerInfo.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; + +/** + * Presentation metadata advertised by an initialized MCP server. + */ +export type McpServerInfo = { + name: string; + title: string | null; + version: string; + description: string | null; + icons: Array | null; + websiteUrl: string | null; +}; diff --git a/src/runtimes/openai/generated/MessagePhase.ts b/src/runtimes/openai/generated/MessagePhase.ts new file mode 100644 index 0000000..9e16021 --- /dev/null +++ b/src/runtimes/openai/generated/MessagePhase.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Classifies an assistant message as interim commentary or final answer text. + * + * Providers do not emit this consistently, so callers must treat `None` as + * "phase unknown" and keep compatibility behavior for legacy models. + */ +export type MessagePhase = "commentary" | "final_answer"; diff --git a/src/runtimes/openai/generated/ModeKind.ts b/src/runtimes/openai/generated/ModeKind.ts new file mode 100644 index 0000000..7d2324a --- /dev/null +++ b/src/runtimes/openai/generated/ModeKind.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Initial collaboration mode to use when the TUI starts. + */ +export type ModeKind = "plan" | "default"; diff --git a/src/runtimes/openai/generated/MultiAgentMode.ts b/src/runtimes/openai/generated/MultiAgentMode.ts new file mode 100644 index 0000000..2c67e5e --- /dev/null +++ b/src/runtimes/openai/generated/MultiAgentMode.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Controls the effective multi-agent delegation instructions for a turn. `custom` means the + * configured mode hint defines the policy instead of a built-in policy. + */ +export type MultiAgentMode = { custom: string } | "explicitRequestOnly" | "proactive"; diff --git a/src/runtimes/openai/generated/NetworkPolicyAmendment.ts b/src/runtimes/openai/generated/NetworkPolicyAmendment.ts new file mode 100644 index 0000000..51348de --- /dev/null +++ b/src/runtimes/openai/generated/NetworkPolicyAmendment.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; + +export type NetworkPolicyAmendment = { host: string; action: NetworkPolicyRuleAction }; diff --git a/src/runtimes/openai/generated/NetworkPolicyRuleAction.ts b/src/runtimes/openai/generated/NetworkPolicyRuleAction.ts new file mode 100644 index 0000000..55ec700 --- /dev/null +++ b/src/runtimes/openai/generated/NetworkPolicyRuleAction.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkPolicyRuleAction = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/ParsedCommand.ts b/src/runtimes/openai/generated/ParsedCommand.ts new file mode 100644 index 0000000..2302ea1 --- /dev/null +++ b/src/runtimes/openai/generated/ParsedCommand.ts @@ -0,0 +1,20 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ParsedCommand = + | { + type: "read"; + cmd: string; + name: string; + /** + * (Best effort) Path to the file being read by the command. When + * possible, this is an absolute path, though when relative, it should + * be resolved against the `cwd`` that will be used to run the command + * to derive the absolute path. + */ + path: string; + } + | { type: "list_files"; cmd: string; path: string | null } + | { type: "search"; cmd: string; query: string | null; path: string | null } + | { type: "unknown"; cmd: string }; diff --git a/src/runtimes/openai/generated/PathUri.ts b/src/runtimes/openai/generated/PathUri.ts new file mode 100644 index 0000000..ba11296 --- /dev/null +++ b/src/runtimes/openai/generated/PathUri.ts @@ -0,0 +1,32 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * An immutable, cross-platform representation of a `file:` URI. + * + * Only the `file:` scheme is currently accepted. Construction validates the + * URL, and the URI cannot be mutated after construction. [`Self::basename`], + * [`Self::parent`], and [`Self::join`] operate on URI path segments without + * interpreting them using the operating system running Codex. Fallback URIs + * created by [`Self::from_abs_path`] are opaque to these lexical operations. + * + * `file:` paths retain their URI spelling so they can be parsed independently + * of the current host, except that Windows drive letters are canonicalized to + * uppercase. A local POSIX `file:` URI can also retain percent-encoded non-UTF-8 + * bytes for lossless native round trips. + * + * Like [VS Code resources], path operations use `/` URI separators on every + * host. Lexical path operations preserve a URL authority without interpreting + * Windows drive or UNC roots from path text. Windows path equality and hashing + * ignore ASCII case, while POSIX paths remain case-sensitive. Native path + * normalization, filesystem aliases, symlinks, and Unicode normalization are + * not resolved. + * + * Serde represents a `PathUri` as its canonical URI string. Deserialization + * accepts only valid `file:` URI strings. These strings round-trip through + * their canonical URL form, including encoded non-UTF-8 path bytes. + * + * [VS Code resources]: https://github.com/microsoft/vscode/blob/main/src/vs/base/common/resources.ts + */ +export type PathUri = string; diff --git a/src/runtimes/openai/generated/Personality.ts b/src/runtimes/openai/generated/Personality.ts new file mode 100644 index 0000000..45165f4 --- /dev/null +++ b/src/runtimes/openai/generated/Personality.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Personality = "none" | "friendly" | "pragmatic"; diff --git a/src/runtimes/openai/generated/PlanType.ts b/src/runtimes/openai/generated/PlanType.ts new file mode 100644 index 0000000..a755b86 --- /dev/null +++ b/src/runtimes/openai/generated/PlanType.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PlanType = + | "free" + | "go" + | "plus" + | "pro" + | "prolite" + | "team" + | "self_serve_business_prolite" + | "self_serve_business_usage_based" + | "business" + | "ent26" + | "enterprise_cbp_automation" + | "enterprise_cbp_usage_based" + | "enterprise" + | "edu" + | "edu_plus" + | "edu_pro" + | "unknown"; diff --git a/src/runtimes/openai/generated/README.md b/src/runtimes/openai/generated/README.md index 2e84430..1cc851b 100644 --- a/src/runtimes/openai/generated/README.md +++ b/src/runtimes/openai/generated/README.md @@ -2,12 +2,20 @@ The TypeScript files in this directory are generated as one atomic schema set. -They correspond to OpenAI app-server runtime version `0.144.5`, which is also pinned by the SDK dependency and container image. +They correspond to OpenAI app-server runtime version `0.150.1`, which is also pinned by the CLI dependency and container image. + +Runtime validation lives in the adjacent `app-server-protocol-*` modules and is typed against this schema. Regenerate them with the matching runtime: ```sh -codex app-server generate-ts --out src/runtimes/openai/generated +generated_dir=src/runtimes/openai/generated +generated_tmp=$(mktemp -d) +trap 'rm -rf "$generated_tmp"' EXIT +vp exec codex app-server generate-ts --experimental --out "$generated_tmp" +fd --extension ts . "$generated_dir" --exec rm +cp -R "$generated_tmp"/. "$generated_dir"/ +vp fmt "$generated_dir" --write ``` Do not split or edit individual generated TypeScript files by hand. diff --git a/src/runtimes/openai/generated/RealtimeConversationVersion.ts b/src/runtimes/openai/generated/RealtimeConversationVersion.ts new file mode 100644 index 0000000..81b8d31 --- /dev/null +++ b/src/runtimes/openai/generated/RealtimeConversationVersion.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RealtimeConversationVersion = "v1" | "v2" | "v3"; diff --git a/src/runtimes/openai/generated/RealtimeOutputModality.ts b/src/runtimes/openai/generated/RealtimeOutputModality.ts new file mode 100644 index 0000000..78e00e7 --- /dev/null +++ b/src/runtimes/openai/generated/RealtimeOutputModality.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RealtimeOutputModality = "text" | "audio"; diff --git a/src/runtimes/openai/generated/RealtimeVoice.ts b/src/runtimes/openai/generated/RealtimeVoice.ts new file mode 100644 index 0000000..bd40db7 --- /dev/null +++ b/src/runtimes/openai/generated/RealtimeVoice.ts @@ -0,0 +1,24 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RealtimeVoice = + | "alloy" + | "arbor" + | "ash" + | "ballad" + | "breeze" + | "cedar" + | "coral" + | "cove" + | "echo" + | "ember" + | "juniper" + | "maple" + | "marin" + | "sage" + | "shimmer" + | "sol" + | "spruce" + | "vale" + | "verse"; diff --git a/src/runtimes/openai/generated/RealtimeVoicesList.ts b/src/runtimes/openai/generated/RealtimeVoicesList.ts new file mode 100644 index 0000000..d18fcdf --- /dev/null +++ b/src/runtimes/openai/generated/RealtimeVoicesList.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RealtimeVoice } from "./RealtimeVoice"; + +export type RealtimeVoicesList = { + v1: Array; + v2: Array; + defaultV1: RealtimeVoice; + defaultV2: RealtimeVoice; +}; diff --git a/src/runtimes/openai/generated/ReasoningEffort.ts b/src/runtimes/openai/generated/ReasoningEffort.ts new file mode 100644 index 0000000..d40f5bd --- /dev/null +++ b/src/runtimes/openai/generated/ReasoningEffort.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning + */ +export type ReasoningEffort = string; diff --git a/src/runtimes/openai/generated/ReasoningItemContent.ts b/src/runtimes/openai/generated/ReasoningItemContent.ts new file mode 100644 index 0000000..1583fa4 --- /dev/null +++ b/src/runtimes/openai/generated/ReasoningItemContent.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReasoningItemContent = + | { type: "reasoning_text"; text: string } + | { type: "text"; text: string }; diff --git a/src/runtimes/openai/generated/ReasoningItemReasoningSummary.ts b/src/runtimes/openai/generated/ReasoningItemReasoningSummary.ts new file mode 100644 index 0000000..cd7cf0a --- /dev/null +++ b/src/runtimes/openai/generated/ReasoningItemReasoningSummary.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReasoningItemReasoningSummary = { type: "summary_text"; text: string }; diff --git a/src/runtimes/openai/generated/ReasoningSummary.ts b/src/runtimes/openai/generated/ReasoningSummary.ts new file mode 100644 index 0000000..d246ac1 --- /dev/null +++ b/src/runtimes/openai/generated/ReasoningSummary.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * A summary of the reasoning performed by the model. This can be useful for + * debugging and understanding the model's reasoning process. + * See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries + */ +export type ReasoningSummary = "auto" | "concise" | "detailed" | "none"; diff --git a/src/runtimes/openai/generated/RequestId.ts b/src/runtimes/openai/generated/RequestId.ts new file mode 100644 index 0000000..8a771bd --- /dev/null +++ b/src/runtimes/openai/generated/RequestId.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RequestId = string | number; diff --git a/src/runtimes/openai/generated/Resource.ts b/src/runtimes/openai/generated/Resource.ts new file mode 100644 index 0000000..680a8eb --- /dev/null +++ b/src/runtimes/openai/generated/Resource.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; + +/** + * A known resource that the server is capable of reading. + */ +export type Resource = { + annotations?: JsonValue; + description?: string; + mimeType?: string; + name: string; + size?: number; + title?: string; + uri: string; + icons?: Array; + _meta?: JsonValue; +}; diff --git a/src/runtimes/openai/generated/ResourceContent.ts b/src/runtimes/openai/generated/ResourceContent.ts new file mode 100644 index 0000000..d40d995 --- /dev/null +++ b/src/runtimes/openai/generated/ResourceContent.ts @@ -0,0 +1,27 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; + +/** + * Contents returned when reading a resource from an MCP server. + */ +export type ResourceContent = + | { + /** + * The URI of this resource. + */ + uri: string; + mimeType?: string; + text: string; + _meta?: JsonValue; + } + | { + /** + * The URI of this resource. + */ + uri: string; + mimeType?: string; + blob: string; + _meta?: JsonValue; + }; diff --git a/src/runtimes/openai/generated/ResourceTemplate.ts b/src/runtimes/openai/generated/ResourceTemplate.ts new file mode 100644 index 0000000..6c31477 --- /dev/null +++ b/src/runtimes/openai/generated/ResourceTemplate.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; + +/** + * A template description for resources available on the server. + */ +export type ResourceTemplate = { + annotations?: JsonValue; + uriTemplate: string; + name: string; + title?: string; + description?: string; + mimeType?: string; +}; diff --git a/src/runtimes/openai/generated/ResponseItem.ts b/src/runtimes/openai/generated/ResponseItem.ts new file mode 100644 index 0000000..b7c211b --- /dev/null +++ b/src/runtimes/openai/generated/ResponseItem.ts @@ -0,0 +1,138 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AgentMessageInputContent } from "./AgentMessageInputContent"; +import type { ContentItem } from "./ContentItem"; +import type { FunctionCallOutputBody } from "./FunctionCallOutputBody"; +import type { InternalChatMessageMetadataPassthrough } from "./InternalChatMessageMetadataPassthrough"; +import type { LocalShellAction } from "./LocalShellAction"; +import type { LocalShellStatus } from "./LocalShellStatus"; +import type { MessagePhase } from "./MessagePhase"; +import type { ReasoningItemContent } from "./ReasoningItemContent"; +import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary"; +import type { ResponseItemId } from "./ResponseItemId"; +import type { WebSearchAction } from "./WebSearchAction"; + +export type ResponseItem = + | { + type: "message"; + id?: ResponseItemId; + role: string; + content: Array; + phase?: MessagePhase; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "agent_message"; + id?: ResponseItemId; + author: string; + recipient: string; + content: Array; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "reasoning"; + id?: ResponseItemId; + summary: Array; + content?: Array; + encrypted_content: string | null; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "local_shell_call"; + /** + * Legacy id field retained for compatibility with older payloads. + */ + id?: ResponseItemId; + /** + * Set when using the Responses API. + */ + call_id: string | null; + status: LocalShellStatus; + action: LocalShellAction; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "function_call"; + id?: ResponseItemId; + name: string; + namespace?: string; + arguments: string; + encrypted_function_args?: Array; + call_id: string; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "tool_search_call"; + id?: ResponseItemId; + call_id: string | null; + status?: string; + execution: string; + arguments: unknown; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "function_call_output"; + id?: ResponseItemId; + call_id?: string; + name?: string; + namespace?: string; + output: FunctionCallOutputBody; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "custom_tool_call"; + id?: ResponseItemId; + status?: string; + call_id: string; + name: string; + namespace?: string; + input: string; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "custom_tool_call_output"; + id?: ResponseItemId; + call_id: string; + name?: string; + output: FunctionCallOutputBody; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "tool_search_output"; + id?: ResponseItemId; + call_id: string | null; + status: string; + execution: string; + tools: unknown[]; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "web_search_call"; + id?: ResponseItemId; + status?: string; + action?: WebSearchAction; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "image_generation_call"; + id?: ResponseItemId; + status: string; + revised_prompt?: string; + result: string; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { + type: "compaction"; + id?: ResponseItemId; + encrypted_content: string; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { type: "compaction_trigger" } + | { + type: "context_compaction"; + id?: ResponseItemId; + encrypted_content?: string; + internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough; + } + | { type: "other" }; diff --git a/src/runtimes/openai/generated/ResponseItemId.ts b/src/runtimes/openai/generated/ResponseItemId.ts new file mode 100644 index 0000000..c4f17ec --- /dev/null +++ b/src/runtimes/openai/generated/ResponseItemId.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * A Responses API item ID. New IDs require an explicit prefix; deserialization + * remains permissive so legacy rollouts can still be read. + */ +export type ResponseItemId = string; diff --git a/src/runtimes/openai/generated/ReviewDecision.ts b/src/runtimes/openai/generated/ReviewDecision.ts new file mode 100644 index 0000000..8251c74 --- /dev/null +++ b/src/runtimes/openai/generated/ReviewDecision.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; +import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; + +/** + * User's decision in response to an ExecApprovalRequest. + */ +export type ReviewDecision = + | "approved" + | { approved_execpolicy_amendment: { proposed_execpolicy_amendment: ExecPolicyAmendment } } + | "approved_for_session" + | "approved_mcp_policy_amendment" + | { network_policy_amendment: { network_policy_amendment: NetworkPolicyAmendment } } + | { denied: { rejection: string } } + | "timed_out" + | "abort"; diff --git a/src/runtimes/openai/generated/ServerNotification.ts b/src/runtimes/openai/generated/ServerNotification.ts new file mode 100644 index 0000000..f285532 --- /dev/null +++ b/src/runtimes/openai/generated/ServerNotification.ts @@ -0,0 +1,193 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification"; +import type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification"; +import type { AccountLoginCompletedNotification } from "./v2/AccountLoginCompletedNotification"; +import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimitsUpdatedNotification"; +import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification"; +import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification"; +import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification"; +import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification"; +import type { CommandExecutionOutputDeltaNotification } from "./v2/CommandExecutionOutputDeltaNotification"; +import type { ConfigWarningNotification } from "./v2/ConfigWarningNotification"; +import type { ContextCompactedNotification } from "./v2/ContextCompactedNotification"; +import type { DeprecationNoticeNotification } from "./v2/DeprecationNoticeNotification"; +import type { EnvironmentConnectionNotification } from "./v2/EnvironmentConnectionNotification"; +import type { ErrorNotification } from "./v2/ErrorNotification"; +import type { ExternalAgentConfigImportCompletedNotification } from "./v2/ExternalAgentConfigImportCompletedNotification"; +import type { ExternalAgentConfigImportProgressNotification } from "./v2/ExternalAgentConfigImportProgressNotification"; +import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDeltaNotification"; +import type { FileChangePatchUpdatedNotification } from "./v2/FileChangePatchUpdatedNotification"; +import type { FsChangedNotification } from "./v2/FsChangedNotification"; +import type { GuardianWarningNotification } from "./v2/GuardianWarningNotification"; +import type { HookCompletedNotification } from "./v2/HookCompletedNotification"; +import type { HookStartedNotification } from "./v2/HookStartedNotification"; +import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification"; +import type { ItemGuardianApprovalReviewCompletedNotification } from "./v2/ItemGuardianApprovalReviewCompletedNotification"; +import type { ItemGuardianApprovalReviewStartedNotification } from "./v2/ItemGuardianApprovalReviewStartedNotification"; +import type { ItemStartedNotification } from "./v2/ItemStartedNotification"; +import type { McpServerEventStreamNotification } from "./v2/McpServerEventStreamNotification"; +import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification"; +import type { McpServerStatusUpdatedNotification } from "./v2/McpServerStatusUpdatedNotification"; +import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification"; +import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification"; +import type { ModelSafetyBufferingUpdatedNotification } from "./v2/ModelSafetyBufferingUpdatedNotification"; +import type { ModelVerificationNotification } from "./v2/ModelVerificationNotification"; +import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification"; +import type { ProcessExitedNotification } from "./v2/ProcessExitedNotification"; +import type { ProcessOutputDeltaNotification } from "./v2/ProcessOutputDeltaNotification"; +import type { ProjectChangedNotification } from "./v2/ProjectChangedNotification"; +import type { RawResponseCompletedNotification } from "./v2/RawResponseCompletedNotification"; +import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification"; +import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification"; +import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification"; +import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification"; +import type { RemoteControlStatusChangedNotification } from "./v2/RemoteControlStatusChangedNotification"; +import type { ServerRequestResolvedNotification } from "./v2/ServerRequestResolvedNotification"; +import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification"; +import type { StrictReviewRequiredNotification } from "./v2/StrictReviewRequiredNotification"; +import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; +import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification"; +import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification"; +import type { ThreadDeletedNotification } from "./v2/ThreadDeletedNotification"; +import type { ThreadGoalClearedNotification } from "./v2/ThreadGoalClearedNotification"; +import type { ThreadGoalUpdatedNotification } from "./v2/ThreadGoalUpdatedNotification"; +import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification"; +import type { ThreadProjectUpdatedNotification } from "./v2/ThreadProjectUpdatedNotification"; +import type { ThreadQueueChangedNotification } from "./v2/ThreadQueueChangedNotification"; +import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosedNotification"; +import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification"; +import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification"; +import type { ThreadRealtimeItemCompletedNotification } from "./v2/ThreadRealtimeItemCompletedNotification"; +import type { ThreadRealtimeItemStartedNotification } from "./v2/ThreadRealtimeItemStartedNotification"; +import type { ThreadRealtimeItemTranscriptDeltaNotification } from "./v2/ThreadRealtimeItemTranscriptDeltaNotification"; +import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification"; +import type { ThreadRealtimeSdpNotification } from "./v2/ThreadRealtimeSdpNotification"; +import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification"; +import type { ThreadRealtimeTranscriptDeltaNotification } from "./v2/ThreadRealtimeTranscriptDeltaNotification"; +import type { ThreadRealtimeTranscriptDoneNotification } from "./v2/ThreadRealtimeTranscriptDoneNotification"; +import type { ThreadRevertedNotification } from "./v2/ThreadRevertedNotification"; +import type { ThreadSettingsUpdatedNotification } from "./v2/ThreadSettingsUpdatedNotification"; +import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification"; +import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification"; +import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification"; +import type { ThreadUnarchivedNotification } from "./v2/ThreadUnarchivedNotification"; +import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification"; +import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification"; +import type { TurnModerationMetadataNotification } from "./v2/TurnModerationMetadataNotification"; +import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification"; +import type { TurnStartedNotification } from "./v2/TurnStartedNotification"; +import type { WarningNotification } from "./v2/WarningNotification"; +import type { WindowsSandboxSetupCompletedNotification } from "./v2/WindowsSandboxSetupCompletedNotification"; +import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldWritableWarningNotification"; + +/** + * Notification sent from the server to the client. + */ +export type ServerNotification = + | { method: "error"; params: ErrorNotification } + | { method: "thread/started"; params: ThreadStartedNotification } + | { method: "thread/status/changed"; params: ThreadStatusChangedNotification } + | { method: "thread/archived"; params: ThreadArchivedNotification } + | { method: "thread/deleted"; params: ThreadDeletedNotification } + | { method: "thread/unarchived"; params: ThreadUnarchivedNotification } + | { method: "thread/closed"; params: ThreadClosedNotification } + | { method: "thread/reverted"; params: ThreadRevertedNotification } + | { method: "skills/changed"; params: SkillsChangedNotification } + | { method: "thread/name/updated"; params: ThreadNameUpdatedNotification } + | { method: "thread/goal/updated"; params: ThreadGoalUpdatedNotification } + | { method: "thread/goal/cleared"; params: ThreadGoalClearedNotification } + | { method: "thread/queue/changed"; params: ThreadQueueChangedNotification } + | { method: "project/changed"; params: ProjectChangedNotification } + | { method: "thread/project/updated"; params: ThreadProjectUpdatedNotification } + | { method: "thread/environment/connected"; params: EnvironmentConnectionNotification } + | { method: "thread/environment/disconnected"; params: EnvironmentConnectionNotification } + | { method: "thread/settings/updated"; params: ThreadSettingsUpdatedNotification } + | { method: "thread/tokenUsage/updated"; params: ThreadTokenUsageUpdatedNotification } + | { method: "turn/started"; params: TurnStartedNotification } + | { method: "hook/started"; params: HookStartedNotification } + | { method: "turn/completed"; params: TurnCompletedNotification } + | { method: "hook/completed"; params: HookCompletedNotification } + | { method: "turn/diff/updated"; params: TurnDiffUpdatedNotification } + | { method: "turn/plan/updated"; params: TurnPlanUpdatedNotification } + | { method: "item/started"; params: ItemStartedNotification } + | { + method: "item/autoApprovalReview/started"; + params: ItemGuardianApprovalReviewStartedNotification; + } + | { + method: "item/autoApprovalReview/completed"; + params: ItemGuardianApprovalReviewCompletedNotification; + } + | { method: "autoApprovalReview/strictReviewRequired"; params: StrictReviewRequiredNotification } + | { method: "item/completed"; params: ItemCompletedNotification } + | { method: "rawResponseItem/completed"; params: RawResponseItemCompletedNotification } + | { method: "rawResponse/completed"; params: RawResponseCompletedNotification } + | { method: "item/agentMessage/delta"; params: AgentMessageDeltaNotification } + | { method: "item/plan/delta"; params: PlanDeltaNotification } + | { method: "command/exec/outputDelta"; params: CommandExecOutputDeltaNotification } + | { method: "process/outputDelta"; params: ProcessOutputDeltaNotification } + | { method: "process/exited"; params: ProcessExitedNotification } + | { method: "item/commandExecution/outputDelta"; params: CommandExecutionOutputDeltaNotification } + | { method: "item/commandExecution/terminalInteraction"; params: TerminalInteractionNotification } + | { method: "item/fileChange/outputDelta"; params: FileChangeOutputDeltaNotification } + | { method: "item/fileChange/patchUpdated"; params: FileChangePatchUpdatedNotification } + | { method: "serverRequest/resolved"; params: ServerRequestResolvedNotification } + | { method: "item/mcpToolCall/progress"; params: McpToolCallProgressNotification } + | { method: "mcpServer/oauthLogin/completed"; params: McpServerOauthLoginCompletedNotification } + | { method: "mcpServer/startupStatus/updated"; params: McpServerStatusUpdatedNotification } + | { method: "mcpServer/event/stream/notification"; params: McpServerEventStreamNotification } + | { method: "account/updated"; params: AccountUpdatedNotification } + | { method: "account/rateLimits/updated"; params: AccountRateLimitsUpdatedNotification } + | { method: "app/list/updated"; params: AppListUpdatedNotification } + | { method: "remoteControl/status/changed"; params: RemoteControlStatusChangedNotification } + | { + method: "externalAgentConfig/import/progress"; + params: ExternalAgentConfigImportProgressNotification; + } + | { + method: "externalAgentConfig/import/completed"; + params: ExternalAgentConfigImportCompletedNotification; + } + | { method: "fs/changed"; params: FsChangedNotification } + | { method: "item/reasoning/summaryTextDelta"; params: ReasoningSummaryTextDeltaNotification } + | { method: "item/reasoning/summaryPartAdded"; params: ReasoningSummaryPartAddedNotification } + | { method: "item/reasoning/textDelta"; params: ReasoningTextDeltaNotification } + | { method: "thread/compacted"; params: ContextCompactedNotification } + | { method: "model/rerouted"; params: ModelReroutedNotification } + | { method: "model/verification"; params: ModelVerificationNotification } + | { method: "turn/moderationMetadata"; params: TurnModerationMetadataNotification } + | { method: "model/safetyBuffering/updated"; params: ModelSafetyBufferingUpdatedNotification } + | { method: "warning"; params: WarningNotification } + | { method: "guardianWarning"; params: GuardianWarningNotification } + | { method: "deprecationNotice"; params: DeprecationNoticeNotification } + | { method: "configWarning"; params: ConfigWarningNotification } + | { method: "fuzzyFileSearch/sessionUpdated"; params: FuzzyFileSearchSessionUpdatedNotification } + | { + method: "fuzzyFileSearch/sessionCompleted"; + params: FuzzyFileSearchSessionCompletedNotification; + } + | { method: "thread/realtime/started"; params: ThreadRealtimeStartedNotification } + | { method: "thread/realtime/itemAdded"; params: ThreadRealtimeItemAddedNotification } + | { method: "thread/realtime/item/started"; params: ThreadRealtimeItemStartedNotification } + | { + method: "thread/realtime/item/transcript/delta"; + params: ThreadRealtimeItemTranscriptDeltaNotification; + } + | { method: "thread/realtime/item/completed"; params: ThreadRealtimeItemCompletedNotification } + | { + method: "thread/realtime/transcript/delta"; + params: ThreadRealtimeTranscriptDeltaNotification; + } + | { method: "thread/realtime/transcript/done"; params: ThreadRealtimeTranscriptDoneNotification } + | { + method: "thread/realtime/outputAudio/delta"; + params: ThreadRealtimeOutputAudioDeltaNotification; + } + | { method: "thread/realtime/sdp"; params: ThreadRealtimeSdpNotification } + | { method: "thread/realtime/error"; params: ThreadRealtimeErrorNotification } + | { method: "thread/realtime/closed"; params: ThreadRealtimeClosedNotification } + | { method: "windows/worldWritableWarning"; params: WindowsWorldWritableWarningNotification } + | { method: "windowsSandbox/setupCompleted"; params: WindowsSandboxSetupCompletedNotification } + | { method: "account/login/completed"; params: AccountLoginCompletedNotification }; diff --git a/src/runtimes/openai/generated/ServerNotificationEnvelope.ts b/src/runtimes/openai/generated/ServerNotificationEnvelope.ts new file mode 100644 index 0000000..ba54079 --- /dev/null +++ b/src/runtimes/openai/generated/ServerNotificationEnvelope.ts @@ -0,0 +1,205 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification"; +import type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification"; +import type { AccountLoginCompletedNotification } from "./v2/AccountLoginCompletedNotification"; +import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimitsUpdatedNotification"; +import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification"; +import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification"; +import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification"; +import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification"; +import type { CommandExecutionOutputDeltaNotification } from "./v2/CommandExecutionOutputDeltaNotification"; +import type { ConfigWarningNotification } from "./v2/ConfigWarningNotification"; +import type { ContextCompactedNotification } from "./v2/ContextCompactedNotification"; +import type { DeprecationNoticeNotification } from "./v2/DeprecationNoticeNotification"; +import type { EnvironmentConnectionNotification } from "./v2/EnvironmentConnectionNotification"; +import type { ErrorNotification } from "./v2/ErrorNotification"; +import type { ExternalAgentConfigImportCompletedNotification } from "./v2/ExternalAgentConfigImportCompletedNotification"; +import type { ExternalAgentConfigImportProgressNotification } from "./v2/ExternalAgentConfigImportProgressNotification"; +import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDeltaNotification"; +import type { FileChangePatchUpdatedNotification } from "./v2/FileChangePatchUpdatedNotification"; +import type { FsChangedNotification } from "./v2/FsChangedNotification"; +import type { GuardianWarningNotification } from "./v2/GuardianWarningNotification"; +import type { HookCompletedNotification } from "./v2/HookCompletedNotification"; +import type { HookStartedNotification } from "./v2/HookStartedNotification"; +import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification"; +import type { ItemGuardianApprovalReviewCompletedNotification } from "./v2/ItemGuardianApprovalReviewCompletedNotification"; +import type { ItemGuardianApprovalReviewStartedNotification } from "./v2/ItemGuardianApprovalReviewStartedNotification"; +import type { ItemStartedNotification } from "./v2/ItemStartedNotification"; +import type { McpServerEventStreamNotification } from "./v2/McpServerEventStreamNotification"; +import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification"; +import type { McpServerStatusUpdatedNotification } from "./v2/McpServerStatusUpdatedNotification"; +import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification"; +import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification"; +import type { ModelSafetyBufferingUpdatedNotification } from "./v2/ModelSafetyBufferingUpdatedNotification"; +import type { ModelVerificationNotification } from "./v2/ModelVerificationNotification"; +import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification"; +import type { ProcessExitedNotification } from "./v2/ProcessExitedNotification"; +import type { ProcessOutputDeltaNotification } from "./v2/ProcessOutputDeltaNotification"; +import type { ProjectChangedNotification } from "./v2/ProjectChangedNotification"; +import type { RawResponseCompletedNotification } from "./v2/RawResponseCompletedNotification"; +import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification"; +import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification"; +import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification"; +import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification"; +import type { RemoteControlStatusChangedNotification } from "./v2/RemoteControlStatusChangedNotification"; +import type { ServerRequestResolvedNotification } from "./v2/ServerRequestResolvedNotification"; +import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification"; +import type { StrictReviewRequiredNotification } from "./v2/StrictReviewRequiredNotification"; +import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; +import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification"; +import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification"; +import type { ThreadDeletedNotification } from "./v2/ThreadDeletedNotification"; +import type { ThreadGoalClearedNotification } from "./v2/ThreadGoalClearedNotification"; +import type { ThreadGoalUpdatedNotification } from "./v2/ThreadGoalUpdatedNotification"; +import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification"; +import type { ThreadProjectUpdatedNotification } from "./v2/ThreadProjectUpdatedNotification"; +import type { ThreadQueueChangedNotification } from "./v2/ThreadQueueChangedNotification"; +import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosedNotification"; +import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification"; +import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification"; +import type { ThreadRealtimeItemCompletedNotification } from "./v2/ThreadRealtimeItemCompletedNotification"; +import type { ThreadRealtimeItemStartedNotification } from "./v2/ThreadRealtimeItemStartedNotification"; +import type { ThreadRealtimeItemTranscriptDeltaNotification } from "./v2/ThreadRealtimeItemTranscriptDeltaNotification"; +import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification"; +import type { ThreadRealtimeSdpNotification } from "./v2/ThreadRealtimeSdpNotification"; +import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification"; +import type { ThreadRealtimeTranscriptDeltaNotification } from "./v2/ThreadRealtimeTranscriptDeltaNotification"; +import type { ThreadRealtimeTranscriptDoneNotification } from "./v2/ThreadRealtimeTranscriptDoneNotification"; +import type { ThreadRevertedNotification } from "./v2/ThreadRevertedNotification"; +import type { ThreadSettingsUpdatedNotification } from "./v2/ThreadSettingsUpdatedNotification"; +import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification"; +import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification"; +import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification"; +import type { ThreadUnarchivedNotification } from "./v2/ThreadUnarchivedNotification"; +import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification"; +import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification"; +import type { TurnModerationMetadataNotification } from "./v2/TurnModerationMetadataNotification"; +import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification"; +import type { TurnStartedNotification } from "./v2/TurnStartedNotification"; +import type { WarningNotification } from "./v2/WarningNotification"; +import type { WindowsSandboxSetupCompletedNotification } from "./v2/WindowsSandboxSetupCompletedNotification"; +import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldWritableWarningNotification"; + +/** + * Server notification envelope sent over app-server transports. + * + * `emitted_at_ms` records when app-server emitted the notification, before it + * is fanned out to individual connections. + */ +export type ServerNotificationEnvelope = { + /** + * Unix timestamp (in milliseconds) when app-server emitted this notification. + * + * Optional so clients can decode notifications from older app-server + * versions. Current app-server versions always populate it. + */ + emittedAtMs?: number; +} & ( + | { method: "error"; params: ErrorNotification } + | { method: "thread/started"; params: ThreadStartedNotification } + | { method: "thread/status/changed"; params: ThreadStatusChangedNotification } + | { method: "thread/archived"; params: ThreadArchivedNotification } + | { method: "thread/deleted"; params: ThreadDeletedNotification } + | { method: "thread/unarchived"; params: ThreadUnarchivedNotification } + | { method: "thread/closed"; params: ThreadClosedNotification } + | { method: "thread/reverted"; params: ThreadRevertedNotification } + | { method: "skills/changed"; params: SkillsChangedNotification } + | { method: "thread/name/updated"; params: ThreadNameUpdatedNotification } + | { method: "thread/goal/updated"; params: ThreadGoalUpdatedNotification } + | { method: "thread/goal/cleared"; params: ThreadGoalClearedNotification } + | { method: "thread/queue/changed"; params: ThreadQueueChangedNotification } + | { method: "project/changed"; params: ProjectChangedNotification } + | { method: "thread/project/updated"; params: ThreadProjectUpdatedNotification } + | { method: "thread/environment/connected"; params: EnvironmentConnectionNotification } + | { method: "thread/environment/disconnected"; params: EnvironmentConnectionNotification } + | { method: "thread/settings/updated"; params: ThreadSettingsUpdatedNotification } + | { method: "thread/tokenUsage/updated"; params: ThreadTokenUsageUpdatedNotification } + | { method: "turn/started"; params: TurnStartedNotification } + | { method: "hook/started"; params: HookStartedNotification } + | { method: "turn/completed"; params: TurnCompletedNotification } + | { method: "hook/completed"; params: HookCompletedNotification } + | { method: "turn/diff/updated"; params: TurnDiffUpdatedNotification } + | { method: "turn/plan/updated"; params: TurnPlanUpdatedNotification } + | { method: "item/started"; params: ItemStartedNotification } + | { + method: "item/autoApprovalReview/started"; + params: ItemGuardianApprovalReviewStartedNotification; + } + | { + method: "item/autoApprovalReview/completed"; + params: ItemGuardianApprovalReviewCompletedNotification; + } + | { method: "autoApprovalReview/strictReviewRequired"; params: StrictReviewRequiredNotification } + | { method: "item/completed"; params: ItemCompletedNotification } + | { method: "rawResponseItem/completed"; params: RawResponseItemCompletedNotification } + | { method: "rawResponse/completed"; params: RawResponseCompletedNotification } + | { method: "item/agentMessage/delta"; params: AgentMessageDeltaNotification } + | { method: "item/plan/delta"; params: PlanDeltaNotification } + | { method: "command/exec/outputDelta"; params: CommandExecOutputDeltaNotification } + | { method: "process/outputDelta"; params: ProcessOutputDeltaNotification } + | { method: "process/exited"; params: ProcessExitedNotification } + | { method: "item/commandExecution/outputDelta"; params: CommandExecutionOutputDeltaNotification } + | { method: "item/commandExecution/terminalInteraction"; params: TerminalInteractionNotification } + | { method: "item/fileChange/outputDelta"; params: FileChangeOutputDeltaNotification } + | { method: "item/fileChange/patchUpdated"; params: FileChangePatchUpdatedNotification } + | { method: "serverRequest/resolved"; params: ServerRequestResolvedNotification } + | { method: "item/mcpToolCall/progress"; params: McpToolCallProgressNotification } + | { method: "mcpServer/oauthLogin/completed"; params: McpServerOauthLoginCompletedNotification } + | { method: "mcpServer/startupStatus/updated"; params: McpServerStatusUpdatedNotification } + | { method: "mcpServer/event/stream/notification"; params: McpServerEventStreamNotification } + | { method: "account/updated"; params: AccountUpdatedNotification } + | { method: "account/rateLimits/updated"; params: AccountRateLimitsUpdatedNotification } + | { method: "app/list/updated"; params: AppListUpdatedNotification } + | { method: "remoteControl/status/changed"; params: RemoteControlStatusChangedNotification } + | { + method: "externalAgentConfig/import/progress"; + params: ExternalAgentConfigImportProgressNotification; + } + | { + method: "externalAgentConfig/import/completed"; + params: ExternalAgentConfigImportCompletedNotification; + } + | { method: "fs/changed"; params: FsChangedNotification } + | { method: "item/reasoning/summaryTextDelta"; params: ReasoningSummaryTextDeltaNotification } + | { method: "item/reasoning/summaryPartAdded"; params: ReasoningSummaryPartAddedNotification } + | { method: "item/reasoning/textDelta"; params: ReasoningTextDeltaNotification } + | { method: "thread/compacted"; params: ContextCompactedNotification } + | { method: "model/rerouted"; params: ModelReroutedNotification } + | { method: "model/verification"; params: ModelVerificationNotification } + | { method: "turn/moderationMetadata"; params: TurnModerationMetadataNotification } + | { method: "model/safetyBuffering/updated"; params: ModelSafetyBufferingUpdatedNotification } + | { method: "warning"; params: WarningNotification } + | { method: "guardianWarning"; params: GuardianWarningNotification } + | { method: "deprecationNotice"; params: DeprecationNoticeNotification } + | { method: "configWarning"; params: ConfigWarningNotification } + | { method: "fuzzyFileSearch/sessionUpdated"; params: FuzzyFileSearchSessionUpdatedNotification } + | { + method: "fuzzyFileSearch/sessionCompleted"; + params: FuzzyFileSearchSessionCompletedNotification; + } + | { method: "thread/realtime/started"; params: ThreadRealtimeStartedNotification } + | { method: "thread/realtime/itemAdded"; params: ThreadRealtimeItemAddedNotification } + | { method: "thread/realtime/item/started"; params: ThreadRealtimeItemStartedNotification } + | { + method: "thread/realtime/item/transcript/delta"; + params: ThreadRealtimeItemTranscriptDeltaNotification; + } + | { method: "thread/realtime/item/completed"; params: ThreadRealtimeItemCompletedNotification } + | { + method: "thread/realtime/transcript/delta"; + params: ThreadRealtimeTranscriptDeltaNotification; + } + | { method: "thread/realtime/transcript/done"; params: ThreadRealtimeTranscriptDoneNotification } + | { + method: "thread/realtime/outputAudio/delta"; + params: ThreadRealtimeOutputAudioDeltaNotification; + } + | { method: "thread/realtime/sdp"; params: ThreadRealtimeSdpNotification } + | { method: "thread/realtime/error"; params: ThreadRealtimeErrorNotification } + | { method: "thread/realtime/closed"; params: ThreadRealtimeClosedNotification } + | { method: "windows/worldWritableWarning"; params: WindowsWorldWritableWarningNotification } + | { method: "windowsSandbox/setupCompleted"; params: WindowsSandboxSetupCompletedNotification } + | { method: "account/login/completed"; params: AccountLoginCompletedNotification } +); diff --git a/src/runtimes/openai/generated/ServerRequest.ts b/src/runtimes/openai/generated/ServerRequest.ts new file mode 100644 index 0000000..f2b11e7 --- /dev/null +++ b/src/runtimes/openai/generated/ServerRequest.ts @@ -0,0 +1,51 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams"; +import type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams"; +import type { RequestId } from "./RequestId"; +import type { AttestationGenerateParams } from "./v2/AttestationGenerateParams"; +import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefreshParams"; +import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams"; +import type { CurrentTimeReadParams } from "./v2/CurrentTimeReadParams"; +import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams"; +import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams"; +import type { McpServerElicitationRequestParams } from "./v2/McpServerElicitationRequestParams"; +import type { PermissionsRequestApprovalParams } from "./v2/PermissionsRequestApprovalParams"; +import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams"; + +/** + * Request initiated from the server and sent to the client. + */ +export type ServerRequest = + | { + method: "item/commandExecution/requestApproval"; + id: RequestId; + params: CommandExecutionRequestApprovalParams; + } + | { + method: "item/fileChange/requestApproval"; + id: RequestId; + params: FileChangeRequestApprovalParams; + } + | { method: "item/tool/requestUserInput"; id: RequestId; params: ToolRequestUserInputParams } + | { + method: "mcpServer/elicitation/request"; + id: RequestId; + params: McpServerElicitationRequestParams; + } + | { + method: "item/permissions/requestApproval"; + id: RequestId; + params: PermissionsRequestApprovalParams; + } + | { method: "item/tool/call"; id: RequestId; params: DynamicToolCallParams } + | { + method: "account/chatgptAuthTokens/refresh"; + id: RequestId; + params: ChatgptAuthTokensRefreshParams; + } + | { method: "attestation/generate"; id: RequestId; params: AttestationGenerateParams } + | { method: "currentTime/read"; id: RequestId; params: CurrentTimeReadParams } + | { method: "applyPatchApproval"; id: RequestId; params: ApplyPatchApprovalParams } + | { method: "execCommandApproval"; id: RequestId; params: ExecCommandApprovalParams }; diff --git a/src/runtimes/openai/generated/SessionSource.ts b/src/runtimes/openai/generated/SessionSource.ts new file mode 100644 index 0000000..592597e --- /dev/null +++ b/src/runtimes/openai/generated/SessionSource.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InternalSessionSource } from "./InternalSessionSource"; +import type { SubAgentSource } from "./SubAgentSource"; + +export type SessionSource = + | "cli" + | "vscode" + | "exec" + | "mcp" + | { custom: string } + | { internal: InternalSessionSource } + | { subagent: SubAgentSource } + | "unknown"; diff --git a/src/runtimes/openai/generated/Settings.ts b/src/runtimes/openai/generated/Settings.ts new file mode 100644 index 0000000..098baf0 --- /dev/null +++ b/src/runtimes/openai/generated/Settings.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReasoningEffort } from "./ReasoningEffort"; + +/** + * Settings for a collaboration mode. + */ +export type Settings = { + model: string; + reasoning_effort: ReasoningEffort | null; + developer_instructions: string | null; +}; diff --git a/src/runtimes/openai/generated/SleepItem.ts b/src/runtimes/openai/generated/SleepItem.ts new file mode 100644 index 0000000..8d9ddaa --- /dev/null +++ b/src/runtimes/openai/generated/SleepItem.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Display item emitted by the interruptible `clock.sleep` tool. + */ +export type SleepItem = { id: string; durationMs: number }; diff --git a/src/runtimes/openai/generated/SubAgentSource.ts b/src/runtimes/openai/generated/SubAgentSource.ts new file mode 100644 index 0000000..ea3a7a7 --- /dev/null +++ b/src/runtimes/openai/generated/SubAgentSource.ts @@ -0,0 +1,20 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AgentPath } from "./AgentPath"; +import type { ThreadId } from "./ThreadId"; + +export type SubAgentSource = + | "review" + | "compact" + | { + thread_spawn: { + parent_thread_id: ThreadId; + depth: number; + agent_path: AgentPath | null; + agent_nickname: string | null; + agent_role: string | null; + }; + } + | "memory_consolidation" + | { other: string }; diff --git a/src/runtimes/openai/generated/ThreadId.ts b/src/runtimes/openai/generated/ThreadId.ts new file mode 100644 index 0000000..801ffb3 --- /dev/null +++ b/src/runtimes/openai/generated/ThreadId.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Identifier for a Codex thread. + * + * Codex-generated thread IDs are UUIDv7, and some use cases rely on that. + */ +export type ThreadId = string; diff --git a/src/runtimes/openai/generated/ThreadMemoryMode.ts b/src/runtimes/openai/generated/ThreadMemoryMode.ts new file mode 100644 index 0000000..74a7e75 --- /dev/null +++ b/src/runtimes/openai/generated/ThreadMemoryMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadMemoryMode = "enabled" | "disabled"; diff --git a/src/runtimes/openai/generated/Tool.ts b/src/runtimes/openai/generated/Tool.ts new file mode 100644 index 0000000..aed8b7a --- /dev/null +++ b/src/runtimes/openai/generated/Tool.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; + +/** + * Definition for a tool the client can call. + */ +export type Tool = { + name: string; + title?: string; + description?: string; + inputSchema: JsonValue; + outputSchema?: JsonValue; + annotations?: JsonValue; + icons?: Array; + _meta?: JsonValue; +}; diff --git a/src/runtimes/openai/generated/Verbosity.ts b/src/runtimes/openai/generated/Verbosity.ts new file mode 100644 index 0000000..8fd97b0 --- /dev/null +++ b/src/runtimes/openai/generated/Verbosity.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Controls output length/detail on GPT-5 models via the Responses API. + * Serialized with lowercase values to match the OpenAI API. + */ +export type Verbosity = "low" | "medium" | "high"; diff --git a/src/runtimes/openai/generated/WebSearchAction.ts b/src/runtimes/openai/generated/WebSearchAction.ts new file mode 100644 index 0000000..3cae0b5 --- /dev/null +++ b/src/runtimes/openai/generated/WebSearchAction.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WebSearchAction = + | { type: "search"; query?: string; queries?: Array } + | { type: "open_page"; url?: string } + | { type: "find_in_page"; url?: string; pattern?: string } + | { type: "other" }; diff --git a/src/runtimes/openai/generated/WebSearchContextSize.ts b/src/runtimes/openai/generated/WebSearchContextSize.ts new file mode 100644 index 0000000..d6feedd --- /dev/null +++ b/src/runtimes/openai/generated/WebSearchContextSize.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WebSearchContextSize = "low" | "medium" | "high"; diff --git a/src/runtimes/openai/generated/WebSearchItem.ts b/src/runtimes/openai/generated/WebSearchItem.ts new file mode 100644 index 0000000..73932cb --- /dev/null +++ b/src/runtimes/openai/generated/WebSearchItem.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; +import type { WebSearchAction } from "./v2/WebSearchAction"; + +export type WebSearchItem = { + id: string; + query: string; + action: WebSearchAction | null; + /** + * Structured search results returned out-of-band by standalone web search. + * + * These stay as opaque JSON at the extension/app-server boundary so new + * result fields and result types can pass through without a Codex release. + */ + results: Array | null; +}; diff --git a/src/runtimes/openai/generated/WebSearchLocation.ts b/src/runtimes/openai/generated/WebSearchLocation.ts new file mode 100644 index 0000000..4f257ee --- /dev/null +++ b/src/runtimes/openai/generated/WebSearchLocation.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WebSearchLocation = { + country: string | null; + region: string | null; + city: string | null; + timezone: string | null; +}; diff --git a/src/runtimes/openai/generated/WebSearchMode.ts b/src/runtimes/openai/generated/WebSearchMode.ts new file mode 100644 index 0000000..0544fd0 --- /dev/null +++ b/src/runtimes/openai/generated/WebSearchMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WebSearchMode = "disabled" | "cached" | "indexed" | "live"; diff --git a/src/runtimes/openai/generated/WebSearchToolConfig.ts b/src/runtimes/openai/generated/WebSearchToolConfig.ts new file mode 100644 index 0000000..518f6d8 --- /dev/null +++ b/src/runtimes/openai/generated/WebSearchToolConfig.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WebSearchContextSize } from "./WebSearchContextSize"; +import type { WebSearchLocation } from "./WebSearchLocation"; + +export type WebSearchToolConfig = { + context_size: WebSearchContextSize | null; + allowed_domains: Array | null; + location: WebSearchLocation | null; +}; diff --git a/src/runtimes/openai/generated/app-server-protocol-client.ts b/src/runtimes/openai/generated/app-server-protocol-client.ts deleted file mode 100644 index 07e998c..0000000 --- a/src/runtimes/openai/generated/app-server-protocol-client.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { expectRecord, parseThread, parseTurn, readString } from "./app-server-protocol-common"; -import type { - ClientRequestMethod, - ClientRequestResult, - InitializeResponse, - ThreadInjectItemsResponse, - ThreadResumeResponse, - ThreadStartResponse, - TurnInterruptResponse, - TurnStartResponse, -} from "./app-server-protocol-types"; - -function parseInitializeResponse(value: unknown): InitializeResponse { - const record = expectRecord(value ?? {}, "initialize result"); - const protocolVersion = readString(record, "protocolVersion"); - - return protocolVersion === null ? {} : { protocolVersion }; -} - -function parseThreadResponse(value: unknown, method: string): ThreadStartResponse { - const record = expectRecord(value, `${method} result`); - - return { - thread: parseThread(record["thread"], `${method} result.thread`), - }; -} - -function parseTurnStartResponse(value: unknown): TurnStartResponse { - const record = expectRecord(value, "turn/start result"); - const turn = expectRecord(record["turn"], "turn/start result.turn"); - - for (const field of [ - "completedAt", - "durationMs", - "error", - "items", - "itemsView", - "startedAt", - "status", - ]) { - if (!Object.hasOwn(turn, field)) { - throw new Error(`turn/start result.turn.${field} is required.`); - } - } - - const parsed = parseTurn(turn, "turn/start result.turn"); - - return { - turn: { - ...parsed, - error: parsed.error ?? null, - } as Required, - }; -} - -function parseTurnInterruptResponse(value: unknown): TurnInterruptResponse { - if (value === undefined || value === null) { - return {}; - } - - const record = expectRecord(value, "turn/interrupt result"); - const turn = record["turn"]; - - return turn === undefined ? {} : { turn: parseTurn(turn, "turn/interrupt result.turn") }; -} - -function parseEmptyResponse(value: unknown, method: string): Record { - expectRecord(value ?? {}, `${method} result`); - return {}; -} - -export const CLIENT_REQUEST_RESULT_PARSERS: { - [Method in ClientRequestMethod]: (value: unknown) => ClientRequestResult[Method]; -} = { - initialize: parseInitializeResponse, - "thread/inject_items": (value: unknown): ThreadInjectItemsResponse => - parseEmptyResponse(value, "thread/inject_items"), - "thread/resume": (value: unknown): ThreadResumeResponse => - parseThreadResponse(value, "thread/resume"), - "thread/start": (value: unknown): ThreadStartResponse => - parseThreadResponse(value, "thread/start"), - "turn/interrupt": parseTurnInterruptResponse, - "turn/start": parseTurnStartResponse, -}; - -export function parseClientRequestResult(method: "initialize", value: unknown): InitializeResponse; -export function parseClientRequestResult( - method: "thread/inject_items", - value: unknown, -): ThreadInjectItemsResponse; -export function parseClientRequestResult( - method: "thread/resume", - value: unknown, -): ThreadResumeResponse; -export function parseClientRequestResult( - method: "thread/start", - value: unknown, -): ThreadStartResponse; -export function parseClientRequestResult( - method: "turn/interrupt", - value: unknown, -): TurnInterruptResponse; -export function parseClientRequestResult(method: "turn/start", value: unknown): TurnStartResponse; -export function parseClientRequestResult( - method: ClientRequestMethod, - value: unknown, -): ClientRequestResult[ClientRequestMethod] { - return CLIENT_REQUEST_RESULT_PARSERS[method](value); -} diff --git a/src/runtimes/openai/generated/app-server-protocol-common.ts b/src/runtimes/openai/generated/app-server-protocol-common.ts deleted file mode 100644 index 6bd2f37..0000000 --- a/src/runtimes/openai/generated/app-server-protocol-common.ts +++ /dev/null @@ -1,480 +0,0 @@ -import type { - FileUpdateChange, - JsonObject, - PatchChangeKind, - TextPosition, - TextRange, - Thread, - ThreadActiveFlag, - ThreadItem, - ThreadStatus, - ThreadTokenUsage, - TokenUsageBreakdown, - Turn, - TurnError, - TurnItemsView, - TurnPlanStep, - TurnPlanStepStatus, - TurnStatus, -} from "./app-server-protocol-types"; - -export function isRecord(value: unknown): value is JsonObject { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -export function expectRecord(value: unknown, label: string): JsonObject { - if (!isRecord(value)) { - throw new Error(`${label} must be an object.`); - } - - return value; -} - -export function readString(value: JsonObject, key: string): string | null { - const entry = value[key]; - return typeof entry === "string" ? entry : null; -} - -export function readRequiredString(value: JsonObject, key: string, label: string): string { - const entry = readString(value, key); - - if (entry === null) { - throw new Error(`${label}.${key} must be a string.`); - } - - return entry; -} - -export function readRequiredBoolean(value: JsonObject, key: string, label: string): boolean { - const entry = value[key]; - - if (typeof entry !== "boolean") { - throw new Error(`${label}.${key} must be a boolean.`); - } - - return entry; -} - -export function readRequiredNumber(value: JsonObject, key: string, label: string): number { - const entry = value[key]; - - if (typeof entry !== "number" || !Number.isFinite(entry)) { - throw new Error(`${label}.${key} must be a finite number.`); - } - - return entry; -} - -function readOptionalString(value: JsonObject, key: string, label: string): string | undefined { - const entry = value[key]; - - if (entry === undefined) { - return undefined; - } - - if (typeof entry === "string") { - return entry; - } - - throw new Error(`${label}.${key} must be a string.`); -} - -function readOptionalNumber(value: JsonObject, key: string, label: string): number | undefined { - const entry = value[key]; - - if (entry === undefined) { - return undefined; - } - - if (typeof entry === "number" && Number.isFinite(entry)) { - return entry; - } - - throw new Error(`${label}.${key} must be a finite number.`); -} - -function readOptionalNullableNumber( - value: JsonObject, - key: string, - label: string, -): number | null | undefined { - const entry = value[key]; - - if (entry === undefined) { - return undefined; - } - - if (entry === null) { - return null; - } - - if (typeof entry === "number" && Number.isFinite(entry)) { - return entry; - } - - throw new Error(`${label}.${key} must be a finite number or null.`); -} - -export function readOptionalNullableString( - value: JsonObject, - key: string, - label: string, -): string | null | undefined { - const entry = value[key]; - - if (entry === undefined) { - return undefined; - } - - if (entry === null || typeof entry === "string") { - return entry; - } - - throw new Error(`${label}.${key} must be a string or null.`); -} - -function parseUint(value: unknown, label: string): number { - if (typeof value !== "number" || !Number.isInteger(value) || value < 0) { - throw new Error(`${label} must be a non-negative integer.`); - } - - return value; -} - -function parseOptionalJsonObject(value: unknown, label: string): JsonObject | undefined { - if (value === undefined) { - return undefined; - } - - return expectRecord(value, label); -} - -function parseOptionalJsonValue(value: unknown, label: string): unknown { - if ( - value === undefined || - value === null || - typeof value === "boolean" || - typeof value === "number" || - typeof value === "string" - ) { - return value; - } - - if (Array.isArray(value)) { - return value.map((entry, index) => parseOptionalJsonValue(entry, `${label}.${String(index)}`)); - } - - if (isRecord(value)) { - return Object.fromEntries( - Object.entries(value).map(([key, entry]) => [key, parseOptionalJsonValue(entry, key)]), - ); - } - - throw new Error(`${label} must be JSON-compatible.`); -} - -function parseTextPosition(value: unknown, label: string): TextPosition { - const record = expectRecord(value, label); - - return { - column: parseUint(record["column"], `${label}.column`), - line: parseUint(record["line"], `${label}.line`), - }; -} - -export function parseOptionalTextRange( - value: JsonObject, - key: string, - label: string, -): TextRange | null | undefined { - const entry = value[key]; - - if (entry === undefined) { - return undefined; - } - - if (entry === null) { - return null; - } - - const record = expectRecord(entry, `${label}.${key}`); - - return { - end: parseTextPosition(record["end"], `${label}.${key}.end`), - start: parseTextPosition(record["start"], `${label}.${key}.start`), - }; -} - -function parsePatchChangeKind(value: unknown, label: string): PatchChangeKind { - const record = expectRecord(value, label); - const type = readRequiredString(record, "type", label); - - if (type === "add" || type === "delete") { - return { type }; - } - - if (type === "update") { - return { - move_path: readOptionalNullableString(record, "move_path", label) ?? null, - type, - }; - } - - throw new Error(`${label}.type is unsupported.`); -} - -function parseFileUpdateChange(value: unknown, label: string): FileUpdateChange { - const record = expectRecord(value, label); - - return { - diff: readRequiredString(record, "diff", label), - kind: parsePatchChangeKind(record["kind"], `${label}.kind`), - path: readRequiredString(record, "path", label), - }; -} - -export function parseFileUpdateChanges(value: unknown, label: string): FileUpdateChange[] { - if (!Array.isArray(value)) { - throw new Error(`${label} must be an array.`); - } - - return value.map((change, index) => parseFileUpdateChange(change, `${label}.${String(index)}`)); -} - -function parseTurnPlanStepStatus(value: unknown, label: string): TurnPlanStepStatus { - if (value === "pending" || value === "inProgress" || value === "completed") { - return value; - } - - throw new Error(`${label} is unsupported.`); -} - -function parseTurnPlanStep(value: unknown, label: string): TurnPlanStep { - const record = expectRecord(value, label); - - return { - status: parseTurnPlanStepStatus(record["status"], `${label}.status`), - step: readRequiredString(record, "step", label), - }; -} - -export function parseTurnPlan(value: unknown, label: string): TurnPlanStep[] { - if (!Array.isArray(value)) { - throw new Error(`${label} must be an array.`); - } - - return value.map((step, index) => parseTurnPlanStep(step, `${label}.${String(index)}`)); -} - -export function parseThreadTurnIds( - value: JsonObject, - label: string, -): { threadId: string; turnId: string } { - return { - threadId: readRequiredString(value, "threadId", label), - turnId: readRequiredString(value, "turnId", label), - }; -} - -export function parseThread(value: unknown, label: string): Thread { - const record = expectRecord(value, label); - const id = readString(record, "id"); - - if (id === null || id.length === 0) { - throw new Error(`${label}.id must be a non-empty string.`); - } - - return { - id, - ...(record["status"] === undefined - ? {} - : { status: parseThreadStatus(record["status"], `${label}.status`) }), - }; -} - -function parseThreadActiveFlag(value: unknown, label: string): ThreadActiveFlag { - if (value === "waitingOnApproval" || value === "waitingOnUserInput") { - return value; - } - - throw new Error(`${label} is unsupported.`); -} - -export function parseThreadStatus(value: unknown, label: string): ThreadStatus { - const record = expectRecord(value, label); - const statusType = readString(record, "type"); - - if (statusType === "notLoaded" || statusType === "idle" || statusType === "systemError") { - return { type: statusType }; - } - - if (statusType === "active") { - const activeFlags = record["activeFlags"]; - - if (!Array.isArray(activeFlags)) { - throw new Error(`${label}.activeFlags must be an array.`); - } - - return { - activeFlags: activeFlags.map((flag, index) => - parseThreadActiveFlag(flag, `${label}.activeFlags.${String(index)}`), - ), - type: "active", - }; - } - - throw new Error(`${label}.type is unsupported.`); -} - -export function parseThreadItem(value: unknown, label: string): ThreadItem { - const record = expectRecord(value, label); - const type = readRequiredString(record, "type", label); - const id = readOptionalString(record, "id", label); - const { id: _id, type: _type, ...rest } = record; - const passthrough = Object.fromEntries( - Object.entries(rest).map(([key, entry]) => [key, parseOptionalJsonValue(entry, key)]), - ); - - return { - ...passthrough, - ...(id === undefined ? {} : { id }), - type, - }; -} - -function parseOptionalThreadItems(value: unknown, label: string): ThreadItem[] | undefined { - if (value === undefined) { - return undefined; - } - - if (!Array.isArray(value)) { - throw new Error(`${label} must be an array.`); - } - - return value.map((item, index) => parseThreadItem(item, `${label}.${String(index)}`)); -} - -function parseTurnStatus(value: unknown, label: string): TurnStatus | undefined { - if (value === undefined) { - return undefined; - } - - if ( - value === "completed" || - value === "interrupted" || - value === "failed" || - value === "inProgress" - ) { - return value; - } - - throw new Error(`${label} is unsupported.`); -} - -function parseTurnItemsView(value: unknown, label: string): TurnItemsView | undefined { - if (value === undefined) { - return undefined; - } - - if (value === "notLoaded" || value === "summary" || value === "full") { - return value; - } - - throw new Error(`${label} is unsupported.`); -} - -export function parseTurn(value: unknown, label: string): Turn { - const record = expectRecord(value, label); - const id = readString(record, "id"); - - if (id === null || id.length === 0) { - throw new Error(`${label}.id must be a non-empty string.`); - } - - const error = - record["error"] === undefined || record["error"] === null - ? null - : parseTurnError(record["error"], `${label}.error`); - const items = parseOptionalThreadItems(record["items"], `${label}.items`); - const itemsView = parseTurnItemsView(record["itemsView"], `${label}.itemsView`); - const startedAt = readOptionalNullableNumber(record, "startedAt", label); - const completedAt = readOptionalNullableNumber(record, "completedAt", label); - const durationMs = readOptionalNullableNumber(record, "durationMs", label); - const status = parseTurnStatus(record["status"], `${label}.status`); - - return { - id, - ...(completedAt === undefined ? {} : { completedAt }), - ...(durationMs === undefined ? {} : { durationMs }), - ...(error === null ? {} : { error }), - ...(items === undefined ? {} : { items }), - ...(itemsView === undefined ? {} : { itemsView }), - ...(startedAt === undefined ? {} : { startedAt }), - ...(status === undefined ? {} : { status }), - }; -} - -export function parseTurnError(value: unknown, label: string): TurnError { - const record = expectRecord(value, label); - - return { - additionalDetails: readOptionalNullableString(record, "additionalDetails", label) ?? null, - message: readRequiredString(record, "message", label), - }; -} - -function parseTokenUsageBreakdown(value: unknown, label: string): TokenUsageBreakdown { - const record = expectRecord(value, label); - - return { - cachedInputTokens: parseUint(record["cachedInputTokens"], `${label}.cachedInputTokens`), - inputTokens: parseUint(record["inputTokens"], `${label}.inputTokens`), - outputTokens: parseUint(record["outputTokens"], `${label}.outputTokens`), - reasoningOutputTokens: parseUint( - record["reasoningOutputTokens"], - `${label}.reasoningOutputTokens`, - ), - totalTokens: parseUint(record["totalTokens"], `${label}.totalTokens`), - }; -} - -export function parseThreadTokenUsage(value: unknown, label: string): ThreadTokenUsage { - const record = expectRecord(value, label); - const modelContextWindow = - record["modelContextWindow"] === null - ? null - : parseUint(record["modelContextWindow"], `${label}.modelContextWindow`); - - return { - last: parseTokenUsageBreakdown(record["last"], `${label}.last`), - modelContextWindow, - total: parseTokenUsageBreakdown(record["total"], `${label}.total`), - }; -} - -export function parseOptionalNotificationString( - value: JsonObject, - key: string, - label: string, -): { readonly [field: string]: string } { - const entry = readOptionalString(value, key, label); - return entry === undefined ? {} : { [key]: entry }; -} - -export function parseOptionalNotificationNumber( - value: JsonObject, - key: string, - label: string, -): { readonly [field: string]: number } { - const entry = readOptionalNumber(value, key, label); - return entry === undefined ? {} : { [key]: entry }; -} - -export function parseOptionalNotificationRecord( - value: JsonObject, - key: string, - label: string, -): { readonly [field: string]: JsonObject } { - const entry = parseOptionalJsonObject(value[key], `${label}.${key}`); - return entry === undefined ? {} : { [key]: entry }; -} diff --git a/src/runtimes/openai/generated/app-server-protocol-server.ts b/src/runtimes/openai/generated/app-server-protocol-server.ts deleted file mode 100644 index 9300070..0000000 --- a/src/runtimes/openai/generated/app-server-protocol-server.ts +++ /dev/null @@ -1,368 +0,0 @@ -import { - expectRecord, - parseFileUpdateChanges, - parseOptionalNotificationNumber, - parseOptionalNotificationString, - parseOptionalTextRange, - parseThread, - parseThreadItem, - parseThreadStatus, - parseThreadTokenUsage, - parseThreadTurnIds, - parseTurn, - parseTurnError, - parseTurnPlan, - readOptionalNullableString, - readRequiredBoolean, - readRequiredNumber, - readRequiredString, -} from "./app-server-protocol-common"; -import type { - AgentMessageDeltaNotification, - ConfigWarningNotification, - ErrorNotification, - FileChangePatchUpdatedNotification, - ItemNotificationBase, - JsonObject, - McpToolCallProgressNotification, - PlanDeltaNotification, - ReasoningSummaryPartAddedNotification, - ReasoningSummaryTextDeltaNotification, - ReasoningTextDeltaNotification, - RemoteControlConnectionStatus, - RemoteControlStatusChangedNotification, - ServerRequestResolvedNotification, - ServerNotificationMethod, - ServerNotificationParams, - ThreadSettingsUpdatedNotification, - TurnDiffUpdatedNotification, - TurnPlanUpdatedNotification, - WarningNotification, -} from "./app-server-protocol-types"; - -function readParams(value: unknown, method: string): JsonObject { - return expectRecord(value ?? {}, `${method} params`); -} - -function parseConfigWarningNotification(value: unknown): ConfigWarningNotification { - const label = "configWarning params"; - const record = readParams(value, "configWarning"); - const details = readOptionalNullableString(record, "details", label); - const path = readOptionalNullableString(record, "path", label); - const range = parseOptionalTextRange(record, "range", label); - - return { - ...(details === undefined ? {} : { details }), - ...(path === undefined ? {} : { path }), - ...(range === undefined ? {} : { range }), - summary: readRequiredString(record, "summary", label), - }; -} - -function parseWarningNotification(value: unknown): WarningNotification { - const label = "warning params"; - const record = readParams(value, "warning"); - - return { - message: readRequiredString(record, "message", label), - threadId: readOptionalNullableString(record, "threadId", label) ?? null, - }; -} - -function parseErrorNotification(value: unknown): ErrorNotification { - const label = "error params"; - const record = readParams(value, "error"); - - return { - error: parseTurnError(record["error"], `${label}.error`), - threadId: readRequiredString(record, "threadId", label), - turnId: readRequiredString(record, "turnId", label), - willRetry: readRequiredBoolean(record, "willRetry", label), - }; -} - -function parseRemoteControlConnectionStatus(value: unknown): RemoteControlConnectionStatus { - if ( - value === "disabled" || - value === "connecting" || - value === "connected" || - value === "errored" - ) { - return value; - } - - throw new Error("remoteControl/status/changed params.status is unsupported."); -} - -function parseRemoteControlStatusChangedNotification( - value: unknown, -): RemoteControlStatusChangedNotification { - const label = "remoteControl/status/changed params"; - const record = readParams(value, "remoteControl/status/changed"); - const environmentId = readOptionalNullableString(record, "environmentId", label); - - return { - ...(environmentId === undefined ? {} : { environmentId }), - installationId: readRequiredString(record, "installationId", label), - serverName: readRequiredString(record, "serverName", label), - status: parseRemoteControlConnectionStatus(record["status"]), - }; -} - -function parseThreadSettingsUpdatedNotification(value: unknown): ThreadSettingsUpdatedNotification { - const label = "thread/settings/updated params"; - const record = readParams(value, "thread/settings/updated"); - - return { - threadId: readRequiredString(record, "threadId", label), - threadSettings: expectRecord(record["threadSettings"], `${label}.threadSettings`), - }; -} - -function parseAgentMessageDeltaNotification(value: unknown): AgentMessageDeltaNotification { - const label = "item/agentMessage/delta params"; - const record = readParams(value, "item/agentMessage/delta"); - - return { - delta: readRequiredString(record, "delta", label), - itemId: readRequiredString(record, "itemId", label), - threadId: readRequiredString(record, "threadId", label), - turnId: readRequiredString(record, "turnId", label), - }; -} - -function parseOptionalToolDeltaNotification( - value: unknown, - method: string, -): { delta?: string; itemId?: string; threadId?: string; turnId?: string } { - const record = readParams(value, method); - - return { - ...parseOptionalNotificationString(record, "delta", `${method} params`), - ...parseOptionalNotificationString(record, "itemId", `${method} params`), - ...parseOptionalNotificationString(record, "threadId", `${method} params`), - ...parseOptionalNotificationString(record, "turnId", `${method} params`), - }; -} - -function parseItemNotificationBase(value: unknown, method: string): ItemNotificationBase { - const record = readParams(value, method); - - return { - ...parseOptionalNotificationNumber(record, "completedAtMs", `${method} params`), - ...(record["item"] === undefined - ? {} - : { item: parseThreadItem(record["item"], `${method} params.item`) }), - ...parseOptionalNotificationString(record, "itemId", `${method} params`), - ...parseOptionalNotificationNumber(record, "startedAtMs", `${method} params`), - ...parseOptionalNotificationString(record, "threadId", `${method} params`), - ...parseOptionalNotificationString(record, "turnId", `${method} params`), - }; -} - -function parsePlanDeltaNotification(value: unknown): PlanDeltaNotification { - const label = "item/plan/delta params"; - const record = readParams(value, "item/plan/delta"); - - return { - delta: readRequiredString(record, "delta", label), - itemId: readRequiredString(record, "itemId", label), - threadId: readRequiredString(record, "threadId", label), - turnId: readRequiredString(record, "turnId", label), - }; -} - -function parseReasoningTextDeltaNotification(value: unknown): ReasoningTextDeltaNotification { - const label = "item/reasoning/textDelta params"; - const record = readParams(value, "item/reasoning/textDelta"); - - return { - contentIndex: readRequiredNumber(record, "contentIndex", label), - delta: readRequiredString(record, "delta", label), - itemId: readRequiredString(record, "itemId", label), - threadId: readRequiredString(record, "threadId", label), - turnId: readRequiredString(record, "turnId", label), - }; -} - -function parseReasoningSummaryPartAddedNotification( - value: unknown, -): ReasoningSummaryPartAddedNotification { - const label = "item/reasoning/summaryPartAdded params"; - const record = readParams(value, "item/reasoning/summaryPartAdded"); - - return { - itemId: readRequiredString(record, "itemId", label), - summaryIndex: readRequiredNumber(record, "summaryIndex", label), - threadId: readRequiredString(record, "threadId", label), - turnId: readRequiredString(record, "turnId", label), - }; -} - -function parseReasoningSummaryTextDeltaNotification( - value: unknown, -): ReasoningSummaryTextDeltaNotification { - const label = "item/reasoning/summaryTextDelta params"; - const record = readParams(value, "item/reasoning/summaryTextDelta"); - - return { - delta: readRequiredString(record, "delta", label), - itemId: readRequiredString(record, "itemId", label), - summaryIndex: readRequiredNumber(record, "summaryIndex", label), - threadId: readRequiredString(record, "threadId", label), - turnId: readRequiredString(record, "turnId", label), - }; -} - -function parseMcpToolCallProgressNotification(value: unknown): McpToolCallProgressNotification { - const label = "item/mcpToolCall/progress params"; - const record = readParams(value, "item/mcpToolCall/progress"); - - return { - itemId: readRequiredString(record, "itemId", label), - message: readRequiredString(record, "message", label), - threadId: readRequiredString(record, "threadId", label), - turnId: readRequiredString(record, "turnId", label), - }; -} - -function parseServerRequestResolvedNotification(value: unknown): ServerRequestResolvedNotification { - const label = "serverRequest/resolved params"; - const record = readParams(value, "serverRequest/resolved"); - const requestId = record["requestId"]; - - if (typeof requestId !== "number" && typeof requestId !== "string") { - throw new Error(`${label}.requestId must be a string or number.`); - } - - return { - requestId, - threadId: readRequiredString(record, "threadId", label), - }; -} - -function parseThreadStarted(value: unknown): ServerNotificationParams["thread/started"] { - const record = readParams(value, "thread/started"); - - return { - thread: parseThread(record["thread"], "thread/started params.thread"), - }; -} - -function parseThreadStatusChanged( - value: unknown, -): ServerNotificationParams["thread/status/changed"] { - const label = "thread/status/changed params"; - const record = readParams(value, "thread/status/changed"); - - return { - status: parseThreadStatus(record["status"], `${label}.status`), - threadId: readRequiredString(record, "threadId", label), - }; -} - -function parseThreadTokenUsageUpdated( - value: unknown, -): ServerNotificationParams["thread/tokenUsage/updated"] { - const label = "thread/tokenUsage/updated params"; - const record = readParams(value, "thread/tokenUsage/updated"); - - return { - threadId: readRequiredString(record, "threadId", label), - tokenUsage: parseThreadTokenUsage(record["tokenUsage"], `${label}.tokenUsage`), - turnId: readRequiredString(record, "turnId", label), - }; -} - -function parseTurnNotification( - value: unknown, - method: "turn/completed" | "turn/started", -): ServerNotificationParams[typeof method] { - const label = `${method} params`; - const record = readParams(value, method); - const turn = parseTurn(record["turn"], `${label}.turn`); - - if ( - method === "turn/completed" && - turn.status !== "completed" && - turn.status !== "failed" && - turn.status !== "interrupted" - ) { - throw new Error(`${label}.turn.status must be terminal.`); - } - - return { - threadId: readRequiredString(record, "threadId", label), - turn, - }; -} - -function parseTurnPlanUpdated(value: unknown): TurnPlanUpdatedNotification { - const label = "turn/plan/updated params"; - const record = readParams(value, "turn/plan/updated"); - - return { - ...parseThreadTurnIds(record, label), - explanation: readOptionalNullableString(record, "explanation", label) ?? null, - plan: parseTurnPlan(record["plan"], `${label}.plan`), - }; -} - -function parseTurnDiffUpdated(value: unknown): TurnDiffUpdatedNotification { - const label = "turn/diff/updated params"; - const record = readParams(value, "turn/diff/updated"); - - return { - ...parseThreadTurnIds(record, label), - diff: readRequiredString(record, "diff", label), - }; -} - -function parseFileChangePatchUpdated(value: unknown): FileChangePatchUpdatedNotification { - const label = "item/fileChange/patchUpdated params"; - const record = readParams(value, "item/fileChange/patchUpdated"); - - return { - ...parseThreadTurnIds(record, label), - changes: parseFileUpdateChanges(record["changes"], `${label}.changes`), - itemId: readRequiredString(record, "itemId", label), - }; -} - -const SERVER_NOTIFICATION_PARAM_PARSERS: { - [Method in ServerNotificationMethod]: (value: unknown) => ServerNotificationParams[Method]; -} = { - configWarning: parseConfigWarningNotification, - error: parseErrorNotification, - "item/agentMessage/delta": parseAgentMessageDeltaNotification, - "item/commandExecution/outputDelta": (value) => - parseOptionalToolDeltaNotification(value, "item/commandExecution/outputDelta"), - "item/completed": (value) => parseItemNotificationBase(value, "item/completed"), - "item/fileChange/outputDelta": (value) => - parseOptionalToolDeltaNotification(value, "item/fileChange/outputDelta"), - "item/fileChange/patchUpdated": parseFileChangePatchUpdated, - "item/mcpToolCall/progress": parseMcpToolCallProgressNotification, - "item/plan/delta": parsePlanDeltaNotification, - "item/reasoning/summaryPartAdded": parseReasoningSummaryPartAddedNotification, - "item/reasoning/summaryTextDelta": parseReasoningSummaryTextDeltaNotification, - "item/reasoning/textDelta": parseReasoningTextDeltaNotification, - "item/started": (value) => parseItemNotificationBase(value, "item/started"), - "remoteControl/status/changed": parseRemoteControlStatusChangedNotification, - "serverRequest/resolved": parseServerRequestResolvedNotification, - "thread/settings/updated": parseThreadSettingsUpdatedNotification, - "thread/started": parseThreadStarted, - "thread/status/changed": parseThreadStatusChanged, - "thread/tokenUsage/updated": parseThreadTokenUsageUpdated, - "turn/completed": (value) => parseTurnNotification(value, "turn/completed"), - "turn/diff/updated": parseTurnDiffUpdated, - "turn/plan/updated": parseTurnPlanUpdated, - "turn/started": (value) => parseTurnNotification(value, "turn/started"), - warning: parseWarningNotification, -}; - -export function parseServerNotificationParams( - method: M, - value: unknown, -): ServerNotificationParams[M] { - return SERVER_NOTIFICATION_PARAM_PARSERS[method](value); -} diff --git a/src/runtimes/openai/generated/app-server-protocol-types.ts b/src/runtimes/openai/generated/app-server-protocol-types.ts deleted file mode 100644 index 1e641dc..0000000 --- a/src/runtimes/openai/generated/app-server-protocol-types.ts +++ /dev/null @@ -1,466 +0,0 @@ -export const OPENAI_APP_SERVER_SCHEMA_VERSION = "0.144.5" as const; - -export type JsonPrimitive = boolean | number | string | null; -export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]; -export type JsonObject = Readonly>; - -export type RequestId = number | string; -export type ApprovalPolicy = "untrusted" | "on-failure" | "on-request" | "never"; -export type ImageDetail = "high" | "original"; -export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access"; -export type SandboxPolicy = SandboxMode | JsonObject; - -export type UserInput = - | { type: "text"; text: string; text_elements: [] } - | { type: "image"; detail?: ImageDetail; url: string } - | { type: "localImage"; detail?: ImageDetail; path: string } - | { type: "skill"; name: string; path: string } - | { type: "mention"; name: string; path: string }; - -export interface InitializeParams { - capabilities: { - experimentalApi: boolean; - requestAttestation: boolean; - } | null; - clientInfo: { - name: string; - title?: string; - version: string; - }; -} - -export interface InitializeResponse { - protocolVersion?: string; -} - -export interface ThreadStartParams { - approvalPolicy?: ApprovalPolicy | null; - approvalsReviewer?: string | JsonObject | null; - baseInstructions?: string | null; - config?: JsonObject | null; - cwd?: string | null; - developerInstructions?: string | null; - ephemeral?: boolean | null; - model?: string | null; - modelProvider?: string | null; - sandbox?: SandboxMode | null; - serviceName?: string | null; - serviceTier?: string | null; - sessionStartSource?: string | null; -} - -export interface ThreadResumeParams extends Omit< - ThreadStartParams, - "ephemeral" | "serviceName" | "sessionStartSource" -> { - threadId: string; -} - -export type ThreadActiveFlag = "waitingOnApproval" | "waitingOnUserInput"; - -export type ThreadStatus = - | { type: "notLoaded" } - | { type: "idle" } - | { type: "systemError" } - | { type: "active"; activeFlags: ThreadActiveFlag[] }; - -export interface Thread { - id: string; - status?: ThreadStatus; -} - -export interface ThreadStartResponse { - thread: Thread; -} - -export interface ThreadResumeResponse { - thread: Thread; -} - -export interface ThreadInjectItemsParams { - items: JsonObject[]; - threadId: string; -} - -export type ThreadInjectItemsResponse = Record; - -export interface TurnStartParams { - approvalPolicy?: ApprovalPolicy | null; - approvalsReviewer?: string | JsonObject | null; - cwd?: string | null; - effort?: string | null; - input: UserInput[]; - model?: string | null; - outputSchema?: JsonValue | null; - sandboxPolicy?: SandboxPolicy | null; - serviceTier?: string | null; - summary?: string | null; - threadId: string; -} - -export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress"; -export type TurnItemsView = "notLoaded" | "summary" | "full"; - -export type PatchChangeKind = - | { type: "add" } - | { type: "delete" } - | { move_path: string | null; type: "update" }; - -export interface FileUpdateChange { - diff: string; - kind: PatchChangeKind; - path: string; -} - -export type TurnPlanStepStatus = "pending" | "inProgress" | "completed"; - -export interface TurnPlanStep { - status: TurnPlanStepStatus; - step: string; -} - -export type ThreadItem = JsonObject & { - id?: string; - type: string; -}; - -export interface Turn { - completedAt?: number | null; - durationMs?: number | null; - error?: TurnError | null; - id: string; - items?: ThreadItem[]; - itemsView?: TurnItemsView; - startedAt?: number | null; - status?: TurnStatus; -} - -export interface TurnStartResponse { - turn: Required; -} - -export interface TurnInterruptParams { - threadId: string; - turnId: string; -} - -export interface TurnInterruptResponse { - turn?: Turn; -} - -export interface TokenUsageBreakdown { - cachedInputTokens: number; - inputTokens: number; - outputTokens: number; - reasoningOutputTokens: number; - totalTokens: number; -} - -export interface ThreadTokenUsage { - last: TokenUsageBreakdown; - modelContextWindow: number | null; - total: TokenUsageBreakdown; -} - -export interface ItemNotificationBase { - completedAtMs?: number; - item?: ThreadItem; - itemId?: string; - startedAtMs?: number; - threadId?: string; - turnId?: string; -} - -export interface PlanDeltaNotification { - delta: string; - itemId: string; - threadId: string; - turnId: string; -} - -export interface TurnPlanUpdatedNotification { - explanation: string | null; - plan: TurnPlanStep[]; - threadId: string; - turnId: string; -} - -export interface TurnDiffUpdatedNotification { - diff: string; - threadId: string; - turnId: string; -} - -export interface FileChangePatchUpdatedNotification { - changes: FileUpdateChange[]; - itemId: string; - threadId: string; - turnId: string; -} - -export interface ReasoningTextDeltaNotification { - contentIndex: number; - delta: string; - itemId: string; - threadId: string; - turnId: string; -} - -export interface ReasoningSummaryPartAddedNotification { - itemId: string; - summaryIndex: number; - threadId: string; - turnId: string; -} - -export interface ReasoningSummaryTextDeltaNotification { - delta: string; - itemId: string; - summaryIndex: number; - threadId: string; - turnId: string; -} - -export interface McpToolCallProgressNotification { - itemId: string; - message: string; - threadId: string; - turnId: string; -} - -export interface ServerRequestResolvedNotification { - requestId: RequestId; - threadId: string; -} - -export interface TextPosition { - column: number; - line: number; -} - -export interface TextRange { - end: TextPosition; - start: TextPosition; -} - -export interface ConfigWarningNotification { - details?: string | null; - path?: string | null; - range?: TextRange | null; - summary: string; -} - -export interface WarningNotification { - message: string; - threadId: string | null; -} - -export interface TurnError { - additionalDetails: string | null; - message: string; -} - -export interface ErrorNotification { - error: TurnError; - threadId: string; - turnId: string; - willRetry: boolean; -} - -export type RemoteControlConnectionStatus = "disabled" | "connecting" | "connected" | "errored"; - -export interface RemoteControlStatusChangedNotification { - environmentId?: string | null; - installationId: string; - serverName: string; - status: RemoteControlConnectionStatus; -} - -export interface ThreadSettingsUpdatedNotification { - threadId: string; - threadSettings: JsonObject; -} - -export interface AgentMessageDeltaNotification { - delta: string; - itemId: string; - threadId: string; - turnId: string; -} - -export interface ServerNotificationParams { - configWarning: ConfigWarningNotification; - error: ErrorNotification; - "item/agentMessage/delta": AgentMessageDeltaNotification; - "item/commandExecution/outputDelta": { - delta?: string; - itemId?: string; - threadId?: string; - turnId?: string; - }; - "item/completed": ItemNotificationBase; - "item/fileChange/outputDelta": { - delta?: string; - itemId?: string; - threadId?: string; - turnId?: string; - }; - "item/fileChange/patchUpdated": FileChangePatchUpdatedNotification; - "item/mcpToolCall/progress": McpToolCallProgressNotification; - "item/plan/delta": PlanDeltaNotification; - "item/reasoning/summaryPartAdded": ReasoningSummaryPartAddedNotification; - "item/reasoning/summaryTextDelta": ReasoningSummaryTextDeltaNotification; - "item/reasoning/textDelta": ReasoningTextDeltaNotification; - "item/started": ItemNotificationBase; - "remoteControl/status/changed": RemoteControlStatusChangedNotification; - "serverRequest/resolved": ServerRequestResolvedNotification; - "thread/settings/updated": ThreadSettingsUpdatedNotification; - "thread/started": { thread: Thread }; - "thread/status/changed": { status: ThreadStatus; threadId: string }; - "thread/tokenUsage/updated": { threadId: string; tokenUsage: ThreadTokenUsage; turnId: string }; - "turn/completed": { threadId: string; turn: Turn }; - "turn/diff/updated": TurnDiffUpdatedNotification; - "turn/plan/updated": TurnPlanUpdatedNotification; - "turn/started": { threadId: string; turn: Turn }; - warning: WarningNotification; -} - -export type ServerNotificationMethod = keyof ServerNotificationParams; - -export interface ClientRequestParams { - initialize: InitializeParams; - "thread/inject_items": ThreadInjectItemsParams; - "thread/resume": ThreadResumeParams; - "thread/start": ThreadStartParams; - "turn/interrupt": TurnInterruptParams; - "turn/start": TurnStartParams; -} - -export interface ClientRequestResult { - initialize: InitializeResponse; - "thread/inject_items": ThreadInjectItemsResponse; - "thread/resume": ThreadResumeResponse; - "thread/start": ThreadStartResponse; - "turn/interrupt": TurnInterruptResponse; - "turn/start": TurnStartResponse; -} - -export type ClientRequestMethod = keyof ClientRequestParams; - -export interface CommandExecutionRequestApprovalResponse { - decision: "accept" | "acceptForSession" | "decline" | "cancel" | JsonObject; -} - -export interface FileChangeRequestApprovalResponse { - decision: "accept" | "acceptForSession" | "decline" | "cancel"; -} - -export interface PermissionsRequestApprovalResponse { - permissions: JsonObject; - scope: "turn" | "session"; - strictAutoReview?: boolean; -} - -export interface ToolRequestUserInputResponse { - answers: Record; -} - -export interface DynamicToolCallResponse { - contentItems: Array< - { type: "inputText"; text: string } | { type: "inputImage"; imageUrl: string } - >; - success: boolean; -} - -export interface ChatgptAuthTokensRefreshResponse { - accessToken: string; - chatgptAccountId: string; - chatgptPlanType: string | null; -} - -export interface AttestationGenerateResponse { - token: string; -} - -export interface CurrentTimeReadResponse { - currentTimeAt: number; -} - -export interface McpServerElicitationRequestResponse { - _meta: JsonValue | null; - action: "accept" | "decline" | "cancel"; - content: JsonValue | null; -} - -export interface ServerRequestParams { - "account/chatgptAuthTokens/refresh": JsonObject; - "attestation/generate": JsonObject; - "currentTime/read": JsonObject; - "item/commandExecution/requestApproval": JsonObject; - "item/fileChange/requestApproval": JsonObject; - "item/permissions/requestApproval": JsonObject; - "item/tool/call": JsonObject; - "item/tool/requestUserInput": JsonObject; - "mcpServer/elicitation/request": JsonObject; -} - -export interface ServerRequestResult { - "account/chatgptAuthTokens/refresh": ChatgptAuthTokensRefreshResponse; - "attestation/generate": AttestationGenerateResponse; - "currentTime/read": CurrentTimeReadResponse; - "item/commandExecution/requestApproval": CommandExecutionRequestApprovalResponse; - "item/fileChange/requestApproval": FileChangeRequestApprovalResponse; - "item/permissions/requestApproval": PermissionsRequestApprovalResponse; - "item/tool/call": DynamicToolCallResponse; - "item/tool/requestUserInput": ToolRequestUserInputResponse; - "mcpServer/elicitation/request": McpServerElicitationRequestResponse; -} - -export type ServerRequestMethod = keyof ServerRequestParams; - -const SERVER_NOTIFICATION_METHODS = new Set([ - "configWarning", - "error", - "item/agentMessage/delta", - "item/commandExecution/outputDelta", - "item/completed", - "item/fileChange/outputDelta", - "item/fileChange/patchUpdated", - "item/mcpToolCall/progress", - "item/plan/delta", - "item/reasoning/summaryPartAdded", - "item/reasoning/summaryTextDelta", - "item/reasoning/textDelta", - "item/started", - "remoteControl/status/changed", - "serverRequest/resolved", - "thread/settings/updated", - "thread/started", - "thread/status/changed", - "thread/tokenUsage/updated", - "turn/completed", - "turn/diff/updated", - "turn/plan/updated", - "turn/started", - "warning", -]); - -const SERVER_REQUEST_METHODS = new Set([ - "account/chatgptAuthTokens/refresh", - "attestation/generate", - "currentTime/read", - "item/commandExecution/requestApproval", - "item/fileChange/requestApproval", - "item/permissions/requestApproval", - "item/tool/call", - "item/tool/requestUserInput", - "mcpServer/elicitation/request", -]); - -export function isServerNotificationMethod(method: string): method is ServerNotificationMethod { - return SERVER_NOTIFICATION_METHODS.has(method); -} - -export function isServerRequestMethod(method: string): method is ServerRequestMethod { - return SERVER_REQUEST_METHODS.has(method); -} diff --git a/src/runtimes/openai/generated/index.ts b/src/runtimes/openai/generated/index.ts new file mode 100644 index 0000000..cb39e54 --- /dev/null +++ b/src/runtimes/openai/generated/index.ts @@ -0,0 +1,100 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +export type { AbsolutePathBuf } from "./AbsolutePathBuf"; +export type { AgentMessageInputContent } from "./AgentMessageInputContent"; +export type { AgentPath } from "./AgentPath"; +export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams"; +export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse"; +export type { AuthMode } from "./AuthMode"; +export type { AutoCompactTokenLimitScope } from "./AutoCompactTokenLimitScope"; +export type { ClientInfo } from "./ClientInfo"; +export type { ClientNotification } from "./ClientNotification"; +export type { ClientRequest } from "./ClientRequest"; +export type { CodexResponseHandoffMode } from "./CodexResponseHandoffMode"; +export type { CollaborationMode } from "./CollaborationMode"; +export type { ContentItem } from "./ContentItem"; +export type { ConversationGitInfo } from "./ConversationGitInfo"; +export type { ConversationSummary } from "./ConversationSummary"; +export type { ConversationTextRole } from "./ConversationTextRole"; +export type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams"; +export type { ExecCommandApprovalResponse } from "./ExecCommandApprovalResponse"; +export type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; +export type { FileChange } from "./FileChange"; +export type { ForcedLoginMethod } from "./ForcedLoginMethod"; +export type { FunctionCallOutputBody } from "./FunctionCallOutputBody"; +export type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem"; +export type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType"; +export type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams"; +export type { FuzzyFileSearchResponse } from "./FuzzyFileSearchResponse"; +export type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult"; +export type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification"; +export type { FuzzyFileSearchSessionStartParams } from "./FuzzyFileSearchSessionStartParams"; +export type { FuzzyFileSearchSessionStartResponse } from "./FuzzyFileSearchSessionStartResponse"; +export type { FuzzyFileSearchSessionStopParams } from "./FuzzyFileSearchSessionStopParams"; +export type { FuzzyFileSearchSessionStopResponse } from "./FuzzyFileSearchSessionStopResponse"; +export type { FuzzyFileSearchSessionUpdateParams } from "./FuzzyFileSearchSessionUpdateParams"; +export type { FuzzyFileSearchSessionUpdateResponse } from "./FuzzyFileSearchSessionUpdateResponse"; +export type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification"; +export type { GetAuthStatusParams } from "./GetAuthStatusParams"; +export type { GetAuthStatusResponse } from "./GetAuthStatusResponse"; +export type { GetConversationSummaryParams } from "./GetConversationSummaryParams"; +export type { GetConversationSummaryResponse } from "./GetConversationSummaryResponse"; +export type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams"; +export type { GitDiffToRemoteResponse } from "./GitDiffToRemoteResponse"; +export type { GitSha } from "./GitSha"; +export type { ImageDetail } from "./ImageDetail"; +export type { ImageGenerationFailure } from "./ImageGenerationFailure"; +export type { ImageGenerationItem } from "./ImageGenerationItem"; +export type { InitializeCapabilities } from "./InitializeCapabilities"; +export type { InitializeParams } from "./InitializeParams"; +export type { InitializeResponse } from "./InitializeResponse"; +export type { InputModality } from "./InputModality"; +export type { InternalChatMessageMetadataPassthrough } from "./InternalChatMessageMetadataPassthrough"; +export type { InternalSessionSource } from "./InternalSessionSource"; +export type { LegacyAppPathString } from "./LegacyAppPathString"; +export type { LocalShellAction } from "./LocalShellAction"; +export type { LocalShellExecAction } from "./LocalShellExecAction"; +export type { LocalShellStatus } from "./LocalShellStatus"; +export type { McpServerInfo } from "./McpServerInfo"; +export type { MessagePhase } from "./MessagePhase"; +export type { ModeKind } from "./ModeKind"; +export type { MultiAgentMode } from "./MultiAgentMode"; +export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; +export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; +export type { ParsedCommand } from "./ParsedCommand"; +export type { PathUri } from "./PathUri"; +export type { Personality } from "./Personality"; +export type { PlanType } from "./PlanType"; +export type { RealtimeConversationVersion } from "./RealtimeConversationVersion"; +export type { RealtimeOutputModality } from "./RealtimeOutputModality"; +export type { RealtimeVoice } from "./RealtimeVoice"; +export type { RealtimeVoicesList } from "./RealtimeVoicesList"; +export type { ReasoningEffort } from "./ReasoningEffort"; +export type { ReasoningItemContent } from "./ReasoningItemContent"; +export type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary"; +export type { ReasoningSummary } from "./ReasoningSummary"; +export type { RequestId } from "./RequestId"; +export type { Resource } from "./Resource"; +export type { ResourceContent } from "./ResourceContent"; +export type { ResourceTemplate } from "./ResourceTemplate"; +export type { ResponseItem } from "./ResponseItem"; +export type { ResponseItemId } from "./ResponseItemId"; +export type { ReviewDecision } from "./ReviewDecision"; +export type { ServerNotification } from "./ServerNotification"; +export type { ServerNotificationEnvelope } from "./ServerNotificationEnvelope"; +export type { ServerRequest } from "./ServerRequest"; +export type { SessionSource } from "./SessionSource"; +export type { Settings } from "./Settings"; +export type { SleepItem } from "./SleepItem"; +export type { SubAgentSource } from "./SubAgentSource"; +export type { ThreadId } from "./ThreadId"; +export type { ThreadMemoryMode } from "./ThreadMemoryMode"; +export type { Tool } from "./Tool"; +export type { Verbosity } from "./Verbosity"; +export type { WebSearchAction } from "./WebSearchAction"; +export type { WebSearchContextSize } from "./WebSearchContextSize"; +export type { WebSearchItem } from "./WebSearchItem"; +export type { WebSearchLocation } from "./WebSearchLocation"; +export type { WebSearchMode } from "./WebSearchMode"; +export type { WebSearchToolConfig } from "./WebSearchToolConfig"; +export * as v2 from "./v2"; diff --git a/src/runtimes/openai/generated/serde_json/JsonValue.ts b/src/runtimes/openai/generated/serde_json/JsonValue.ts new file mode 100644 index 0000000..dbf7173 --- /dev/null +++ b/src/runtimes/openai/generated/serde_json/JsonValue.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type JsonValue = + | number + | string + | boolean + | Array + | { [key in string]?: JsonValue } + | null; diff --git a/src/runtimes/openai/generated/v2/Account.ts b/src/runtimes/openai/generated/v2/Account.ts new file mode 100644 index 0000000..fa7efca --- /dev/null +++ b/src/runtimes/openai/generated/v2/Account.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PlanType } from "../PlanType"; + +export type Account = + | { type: "apiKey" } + | { type: "chatgpt"; email: string | null; planType: PlanType } + | { type: "amazonBedrock"; usesCodexManagedCredentials: boolean }; diff --git a/src/runtimes/openai/generated/v2/AccountLoginCompletedNotification.ts b/src/runtimes/openai/generated/v2/AccountLoginCompletedNotification.ts new file mode 100644 index 0000000..b5ad74e --- /dev/null +++ b/src/runtimes/openai/generated/v2/AccountLoginCompletedNotification.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DesktopOnboardingEntrypoint } from "./DesktopOnboardingEntrypoint"; + +export type AccountLoginCompletedNotification = { + loginId: string | null; + success: boolean; + error: string | null; + onboardingEntrypoint: DesktopOnboardingEntrypoint | null; +}; diff --git a/src/runtimes/openai/generated/v2/AccountRateLimitsUpdatedNotification.ts b/src/runtimes/openai/generated/v2/AccountRateLimitsUpdatedNotification.ts new file mode 100644 index 0000000..163b608 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AccountRateLimitsUpdatedNotification.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimitSnapshot } from "./RateLimitSnapshot"; + +/** + * Sparse rolling rate-limit update. + * + * Clients should merge available values into the most recent `account/rateLimits/read` response + * or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and + * does not clear a previously observed value. + */ +export type AccountRateLimitsUpdatedNotification = { rateLimits: RateLimitSnapshot }; diff --git a/src/runtimes/openai/generated/v2/AccountTokenUsageDailyBucket.ts b/src/runtimes/openai/generated/v2/AccountTokenUsageDailyBucket.ts new file mode 100644 index 0000000..a9c3f9a --- /dev/null +++ b/src/runtimes/openai/generated/v2/AccountTokenUsageDailyBucket.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AccountTokenUsageDailyBucket = { startDate: string; tokens: bigint }; diff --git a/src/runtimes/openai/generated/v2/AccountTokenUsageSummary.ts b/src/runtimes/openai/generated/v2/AccountTokenUsageSummary.ts new file mode 100644 index 0000000..23adb9e --- /dev/null +++ b/src/runtimes/openai/generated/v2/AccountTokenUsageSummary.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AccountTokenUsageSummary = { + lifetimeTokens: bigint | null; + peakDailyTokens: bigint | null; + longestRunningTurnSec: bigint | null; + currentStreakDays: bigint | null; + longestStreakDays: bigint | null; +}; diff --git a/src/runtimes/openai/generated/v2/AccountUpdatedNotification.ts b/src/runtimes/openai/generated/v2/AccountUpdatedNotification.ts new file mode 100644 index 0000000..e1862ac --- /dev/null +++ b/src/runtimes/openai/generated/v2/AccountUpdatedNotification.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AuthMode } from "../AuthMode"; +import type { PlanType } from "../PlanType"; + +export type AccountUpdatedNotification = { authMode: AuthMode | null; planType: PlanType | null }; diff --git a/src/runtimes/openai/generated/v2/ActivePermissionProfile.ts b/src/runtimes/openai/generated/v2/ActivePermissionProfile.ts new file mode 100644 index 0000000..48f258a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ActivePermissionProfile.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActivePermissionProfile = { + /** + * Identifier from `default_permissions` or the implicit built-in default, + * such as `:workspace` or a user-defined `[permissions.]` profile. + */ + id: string; + /** + * Parent profile identifier from the selected permissions profile's + * `extends` setting, when present. + */ + extends: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/AddCreditsNudgeCreditType.ts b/src/runtimes/openai/generated/v2/AddCreditsNudgeCreditType.ts new file mode 100644 index 0000000..70498d6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AddCreditsNudgeCreditType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AddCreditsNudgeCreditType = "credits" | "usage_limit"; diff --git a/src/runtimes/openai/generated/v2/AddCreditsNudgeEmailStatus.ts b/src/runtimes/openai/generated/v2/AddCreditsNudgeEmailStatus.ts new file mode 100644 index 0000000..2b62da6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AddCreditsNudgeEmailStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AddCreditsNudgeEmailStatus = "sent" | "cooldown_active"; diff --git a/src/runtimes/openai/generated/v2/AdditionalContextEntry.ts b/src/runtimes/openai/generated/v2/AdditionalContextEntry.ts new file mode 100644 index 0000000..82edbaa --- /dev/null +++ b/src/runtimes/openai/generated/v2/AdditionalContextEntry.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AdditionalContextKind } from "./AdditionalContextKind"; + +export type AdditionalContextEntry = { value: string; kind: AdditionalContextKind }; diff --git a/src/runtimes/openai/generated/v2/AdditionalContextKind.ts b/src/runtimes/openai/generated/v2/AdditionalContextKind.ts new file mode 100644 index 0000000..cd60bd7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AdditionalContextKind.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AdditionalContextKind = "untrusted" | "application"; diff --git a/src/runtimes/openai/generated/v2/AdditionalFileSystemPermissions.ts b/src/runtimes/openai/generated/v2/AdditionalFileSystemPermissions.ts new file mode 100644 index 0000000..6b09707 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AdditionalFileSystemPermissions.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LegacyAppPathString } from "../LegacyAppPathString"; +import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry"; + +export type AdditionalFileSystemPermissions = { + /** + * This will be removed in favor of `entries`. + */ + read: Array | null; + /** + * This will be removed in favor of `entries`. + */ + write: Array | null; + globScanMaxDepth?: number; + entries?: Array; +}; diff --git a/src/runtimes/openai/generated/v2/AdditionalNetworkPermissions.ts b/src/runtimes/openai/generated/v2/AdditionalNetworkPermissions.ts new file mode 100644 index 0000000..9349aa6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AdditionalNetworkPermissions.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AdditionalNetworkPermissions = { enabled: boolean | null }; diff --git a/src/runtimes/openai/generated/v2/AdditionalPermissionProfile.ts b/src/runtimes/openai/generated/v2/AdditionalPermissionProfile.ts new file mode 100644 index 0000000..108472d --- /dev/null +++ b/src/runtimes/openai/generated/v2/AdditionalPermissionProfile.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; +import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; + +export type AdditionalPermissionProfile = { + /** + * Partial overlay used for per-command permission requests. + */ + network: AdditionalNetworkPermissions | null; + fileSystem: AdditionalFileSystemPermissions | null; +}; diff --git a/src/runtimes/openai/generated/v2/AgentMessageDelivery.ts b/src/runtimes/openai/generated/v2/AgentMessageDelivery.ts new file mode 100644 index 0000000..bc6547e --- /dev/null +++ b/src/runtimes/openai/generated/v2/AgentMessageDelivery.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AgentMessageDelivery = "async"; diff --git a/src/runtimes/openai/generated/v2/AgentMessageDeltaNotification.ts b/src/runtimes/openai/generated/v2/AgentMessageDeltaNotification.ts new file mode 100644 index 0000000..1beb2e9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AgentMessageDeltaNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AgentMessageDeltaNotification = { + threadId: string; + turnId: string; + itemId: string; + delta: string; +}; diff --git a/src/runtimes/openai/generated/v2/AllowDenyRequirement.ts b/src/runtimes/openai/generated/v2/AllowDenyRequirement.ts new file mode 100644 index 0000000..ec13796 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AllowDenyRequirement.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AllowDenyRequirement = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/v2/AnalyticsConfig.ts b/src/runtimes/openai/generated/v2/AnalyticsConfig.ts new file mode 100644 index 0000000..4e36555 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AnalyticsConfig.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type AnalyticsConfig = { enabled: boolean | null } & { + [key in string]?: + | number + | string + | boolean + | Array + | { [key in string]?: JsonValue } + | null; +}; diff --git a/src/runtimes/openai/generated/v2/AppBranding.ts b/src/runtimes/openai/generated/v2/AppBranding.ts new file mode 100644 index 0000000..8a2999e --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppBranding.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - app metadata returned by app-list APIs. + */ +export type AppBranding = { + category: string | null; + developer: string | null; + website: string | null; + privacyPolicy: string | null; + termsOfService: string | null; + isDiscoverableApp: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/AppInfo.ts b/src/runtimes/openai/generated/v2/AppInfo.ts new file mode 100644 index 0000000..8838c0a --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppInfo.ts @@ -0,0 +1,34 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppBranding } from "./AppBranding"; +import type { AppMetadata } from "./AppMetadata"; + +/** + * EXPERIMENTAL - app metadata returned by app-list APIs. + */ +export type AppInfo = { + id: string; + name: string; + description: string | null; + logoUrl: string | null; + logoUrlDark: string | null; + iconAssets: { [key in string]?: string } | null; + iconDarkAssets: { [key in string]?: string } | null; + distributionChannel: string | null; + branding: AppBranding | null; + appMetadata: AppMetadata | null; + labels: { [key in string]?: string } | null; + installUrl: string | null; + isAccessible: boolean; + /** + * Whether this app is enabled in config.toml. + * Example: + * ```toml + * [apps.bad_app] + * enabled = false + * ``` + */ + isEnabled: boolean; + pluginDisplayNames: Array; +}; diff --git a/src/runtimes/openai/generated/v2/AppListUpdatedNotification.ts b/src/runtimes/openai/generated/v2/AppListUpdatedNotification.ts new file mode 100644 index 0000000..794ce09 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppListUpdatedNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppInfo } from "./AppInfo"; + +/** + * EXPERIMENTAL - notification emitted when the app list changes. + */ +export type AppListUpdatedNotification = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/AppMetadata.ts b/src/runtimes/openai/generated/v2/AppMetadata.ts new file mode 100644 index 0000000..1377fd2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppMetadata.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppReview } from "./AppReview"; +import type { AppScreenshot } from "./AppScreenshot"; + +export type AppMetadata = { + review: AppReview | null; + categories: Array | null; + subCategories: Array | null; + seoDescription: string | null; + screenshots: Array | null; + developer: string | null; + version: string | null; + versionId: string | null; + versionNotes: string | null; + firstPartyRequiresInstall: boolean | null; + showInComposerWhenUnlinked: boolean | null; +}; diff --git a/src/runtimes/openai/generated/v2/AppReview.ts b/src/runtimes/openai/generated/v2/AppReview.ts new file mode 100644 index 0000000..96e14b7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppReview.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AppReview = { status: string }; diff --git a/src/runtimes/openai/generated/v2/AppScreenshot.ts b/src/runtimes/openai/generated/v2/AppScreenshot.ts new file mode 100644 index 0000000..f538ccc --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppScreenshot.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AppScreenshot = { url: string | null; fileId: string | null; userPrompt: string }; diff --git a/src/runtimes/openai/generated/v2/AppSummary.ts b/src/runtimes/openai/generated/v2/AppSummary.ts new file mode 100644 index 0000000..7fddf8c --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppSummary.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - app metadata summary for plugin responses. + */ +export type AppSummary = { + id: string; + name: string; + description: string | null; + installUrl: string | null; + category: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/AppTemplateSummary.ts b/src/runtimes/openai/generated/v2/AppTemplateSummary.ts new file mode 100644 index 0000000..cabea63 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppTemplateSummary.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppTemplateUnavailableReason } from "./AppTemplateUnavailableReason"; + +export type AppTemplateSummary = { + templateId: string; + name: string; + description: string | null; + category: string | null; + canonicalConnectorId: string | null; + logoUrl: string | null; + logoUrlDark: string | null; + materializedAppIds: Array; + reason: AppTemplateUnavailableReason | null; +}; diff --git a/src/runtimes/openai/generated/v2/AppTemplateUnavailableReason.ts b/src/runtimes/openai/generated/v2/AppTemplateUnavailableReason.ts new file mode 100644 index 0000000..5630567 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppTemplateUnavailableReason.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AppTemplateUnavailableReason = "NOT_CONFIGURED_FOR_WORKSPACE" | "NO_ACTIVE_WORKSPACE"; diff --git a/src/runtimes/openai/generated/v2/AppToolApproval.ts b/src/runtimes/openai/generated/v2/AppToolApproval.ts new file mode 100644 index 0000000..6704ef0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppToolApproval.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AppToolApproval = "auto" | "prompt" | "writes" | "approve"; diff --git a/src/runtimes/openai/generated/v2/AppToolSummary.ts b/src/runtimes/openai/generated/v2/AppToolSummary.ts new file mode 100644 index 0000000..138ad25 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppToolSummary.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - metadata returned by app/read. + */ +export type AppToolSummary = { + name: string; + title: string | null; + description: string; + isEnabled: boolean; + disabledReason: string | null; + isReadOnly: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/AppToolsConfig.ts b/src/runtimes/openai/generated/v2/AppToolsConfig.ts new file mode 100644 index 0000000..f7f3650 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppToolsConfig.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppToolApproval } from "./AppToolApproval"; + +export type AppToolsConfig = { + [key in string]?: { enabled: boolean | null; approval_mode: AppToolApproval | null }; +}; diff --git a/src/runtimes/openai/generated/v2/ApprovalsReviewer.ts b/src/runtimes/openai/generated/v2/ApprovalsReviewer.ts new file mode 100644 index 0000000..1d93294 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ApprovalsReviewer.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Configures who approval requests are routed to for review. Examples + * include sandbox escapes, blocked network access, MCP approval prompts, and + * ARC escalations. Defaults to `user`. `auto_review` uses a carefully + * prompted subagent to gather relevant context and apply a risk-based + * decision framework before approving or denying the request. + */ +export type ApprovalsReviewer = "user" | "auto_review" | "guardian_subagent"; diff --git a/src/runtimes/openai/generated/v2/AppsConfig.ts b/src/runtimes/openai/generated/v2/AppsConfig.ts new file mode 100644 index 0000000..1fee163 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppsConfig.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppToolApproval } from "./AppToolApproval"; +import type { AppToolsConfig } from "./AppToolsConfig"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AppsDefaultConfig } from "./AppsDefaultConfig"; + +export type AppsConfig = { _default: AppsDefaultConfig | null } & { + [key in string]?: { + enabled: boolean; + approvals_reviewer: ApprovalsReviewer | null; + destructive_enabled: boolean | null; + open_world_enabled: boolean | null; + default_tools_approval_mode: AppToolApproval | null; + default_tools_enabled: boolean | null; + tools: AppToolsConfig | null; + }; +}; diff --git a/src/runtimes/openai/generated/v2/AppsDefaultConfig.ts b/src/runtimes/openai/generated/v2/AppsDefaultConfig.ts new file mode 100644 index 0000000..9ca967e --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppsDefaultConfig.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppToolApproval } from "./AppToolApproval"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; + +export type AppsDefaultConfig = { + enabled: boolean; + approvals_reviewer: ApprovalsReviewer | null; + destructive_enabled: boolean; + open_world_enabled: boolean; + default_tools_approval_mode: AppToolApproval | null; +}; diff --git a/src/runtimes/openai/generated/v2/AppsInstalledParams.ts b/src/runtimes/openai/generated/v2/AppsInstalledParams.ts new file mode 100644 index 0000000..fab4440 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppsInstalledParams.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Read the committed installed connector runtime snapshot. + */ +export type AppsInstalledParams = { + /** + * Optional loaded thread id used to evaluate effective app configuration. + */ + threadId?: string | null; + /** + * When true and Apps are permitted, refresh and publish the hosted connector runtime tool + * snapshot first. + */ + forceRefresh?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/AppsInstalledResponse.ts b/src/runtimes/openai/generated/v2/AppsInstalledResponse.ts new file mode 100644 index 0000000..69af3df --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppsInstalledResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InstalledApp } from "./InstalledApp"; + +/** + * The installed connectors in one committed runtime snapshot. + */ +export type AppsInstalledResponse = { apps: Array }; diff --git a/src/runtimes/openai/generated/v2/AppsListParams.ts b/src/runtimes/openai/generated/v2/AppsListParams.ts new file mode 100644 index 0000000..e85212c --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppsListParams.ts @@ -0,0 +1,25 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - list available apps/connectors. + */ +export type AppsListParams = { + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size; defaults to a reasonable server-side value. + */ + limit?: number | null; + /** + * Optional thread id used to evaluate app feature gating from that thread's config. + */ + threadId?: string | null; + /** + * When true, bypass app caches and fetch the latest data from sources. + */ + forceRefetch?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/AppsListResponse.ts b/src/runtimes/openai/generated/v2/AppsListResponse.ts new file mode 100644 index 0000000..0ac62a4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppsListResponse.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppInfo } from "./AppInfo"; + +/** + * EXPERIMENTAL - app list response. + */ +export type AppsListResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * If None, there are no more items to return. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/AppsReadParams.ts b/src/runtimes/openai/generated/v2/AppsReadParams.ts new file mode 100644 index 0000000..1345cb3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppsReadParams.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - read metadata for specific apps/connectors. + */ +export type AppsReadParams = { + /** + * App ids to read. The server accepts at most 100 ids and deduplicates repeated ids while + * preserving their first-request order. + */ + appIds: Array; + /** + * Optional loaded thread id used to evaluate effective app configuration. + */ + threadId?: string | null; + /** + * When true, include display-only public tool summaries in the returned metadata. + */ + includeTools?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/AppsReadResponse.ts b/src/runtimes/openai/generated/v2/AppsReadResponse.ts new file mode 100644 index 0000000..e90e772 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AppsReadResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConnectorMetadata } from "./ConnectorMetadata"; + +/** + * EXPERIMENTAL - app/read response. + */ +export type AppsReadResponse = { apps: Array; missingAppIds: Array }; diff --git a/src/runtimes/openai/generated/v2/AskForApproval.ts b/src/runtimes/openai/generated/v2/AskForApproval.ts new file mode 100644 index 0000000..d52ecbd --- /dev/null +++ b/src/runtimes/openai/generated/v2/AskForApproval.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AskForApproval = + | "untrusted" + | "on-request" + | { + granular: { + sandbox_approval: boolean; + rules: boolean; + skill_approval: boolean; + request_permissions: boolean; + mcp_elicitations: boolean; + }; + } + | "never"; diff --git a/src/runtimes/openai/generated/v2/AttestationGenerateParams.ts b/src/runtimes/openai/generated/v2/AttestationGenerateParams.ts new file mode 100644 index 0000000..0e87e7d --- /dev/null +++ b/src/runtimes/openai/generated/v2/AttestationGenerateParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AttestationGenerateParams = Record; diff --git a/src/runtimes/openai/generated/v2/AttestationGenerateResponse.ts b/src/runtimes/openai/generated/v2/AttestationGenerateResponse.ts new file mode 100644 index 0000000..0dd4b0a --- /dev/null +++ b/src/runtimes/openai/generated/v2/AttestationGenerateResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AttestationGenerateResponse = { + /** + * Opaque client attestation token. + */ + token: string; +}; diff --git a/src/runtimes/openai/generated/v2/AutoReviewDecisionSource.ts b/src/runtimes/openai/generated/v2/AutoReviewDecisionSource.ts new file mode 100644 index 0000000..8806981 --- /dev/null +++ b/src/runtimes/openai/generated/v2/AutoReviewDecisionSource.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * [UNSTABLE] Source that produced a terminal approval auto-review decision. + */ +export type AutoReviewDecisionSource = "agent"; diff --git a/src/runtimes/openai/generated/v2/AutoReviewRequirements.ts b/src/runtimes/openai/generated/v2/AutoReviewRequirements.ts new file mode 100644 index 0000000..57bd31a --- /dev/null +++ b/src/runtimes/openai/generated/v2/AutoReviewRequirements.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AutoReviewRequirements = { + requiredOnModels: Array | null; + ignoreRules: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/AwsCredentialType.ts b/src/runtimes/openai/generated/v2/AwsCredentialType.ts new file mode 100644 index 0000000..cc88efa --- /dev/null +++ b/src/runtimes/openai/generated/v2/AwsCredentialType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AwsCredentialType = "accessKeys" | "bedrockApiKey"; diff --git a/src/runtimes/openai/generated/v2/BedrockAwsProfile.ts b/src/runtimes/openai/generated/v2/BedrockAwsProfile.ts new file mode 100644 index 0000000..53eac31 --- /dev/null +++ b/src/runtimes/openai/generated/v2/BedrockAwsProfile.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BedrockAwsProfile = { name: string; region: string | null }; diff --git a/src/runtimes/openai/generated/v2/BedrockDiscoverParams.ts b/src/runtimes/openai/generated/v2/BedrockDiscoverParams.ts new file mode 100644 index 0000000..61ab5fc --- /dev/null +++ b/src/runtimes/openai/generated/v2/BedrockDiscoverParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BedrockDiscoverParams = Record; diff --git a/src/runtimes/openai/generated/v2/BedrockDiscoverResponse.ts b/src/runtimes/openai/generated/v2/BedrockDiscoverResponse.ts new file mode 100644 index 0000000..8222e42 --- /dev/null +++ b/src/runtimes/openai/generated/v2/BedrockDiscoverResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BedrockAwsProfile } from "./BedrockAwsProfile"; +import type { BedrockEnvironmentCredential } from "./BedrockEnvironmentCredential"; + +export type BedrockDiscoverResponse = { + profiles: Array; + environmentCredentials: Array; +}; diff --git a/src/runtimes/openai/generated/v2/BedrockEnvironmentCredential.ts b/src/runtimes/openai/generated/v2/BedrockEnvironmentCredential.ts new file mode 100644 index 0000000..52b1347 --- /dev/null +++ b/src/runtimes/openai/generated/v2/BedrockEnvironmentCredential.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AwsCredentialType } from "./AwsCredentialType"; + +export type BedrockEnvironmentCredential = { type: AwsCredentialType; region: string | null }; diff --git a/src/runtimes/openai/generated/v2/BedrockSetupParams.ts b/src/runtimes/openai/generated/v2/BedrockSetupParams.ts new file mode 100644 index 0000000..5325b06 --- /dev/null +++ b/src/runtimes/openai/generated/v2/BedrockSetupParams.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BedrockSetupParams = + | { type: "profile"; profile: string; region: string } + | { type: "environment"; region: string }; diff --git a/src/runtimes/openai/generated/v2/BedrockSetupResponse.ts b/src/runtimes/openai/generated/v2/BedrockSetupResponse.ts new file mode 100644 index 0000000..c76ba9b --- /dev/null +++ b/src/runtimes/openai/generated/v2/BedrockSetupResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BedrockSetupResponse = Record; diff --git a/src/runtimes/openai/generated/v2/BrowserUseAccessApprovalLifetime.ts b/src/runtimes/openai/generated/v2/BrowserUseAccessApprovalLifetime.ts new file mode 100644 index 0000000..f86b59e --- /dev/null +++ b/src/runtimes/openai/generated/v2/BrowserUseAccessApprovalLifetime.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BrowserUseAccessApprovalLifetime = "turn" | "thread"; diff --git a/src/runtimes/openai/generated/v2/BrowserUseConfig.ts b/src/runtimes/openai/generated/v2/BrowserUseConfig.ts new file mode 100644 index 0000000..b8f159f --- /dev/null +++ b/src/runtimes/openai/generated/v2/BrowserUseConfig.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BrowserUseOriginPolicyConfig } from "./BrowserUseOriginPolicyConfig"; + +export type BrowserUseConfig = { + allow_history_access: boolean | null; + default_origin_policy: BrowserUseOriginPolicyConfig | null; + origins: { [key in string]?: BrowserUseOriginPolicyConfig } | null; +}; diff --git a/src/runtimes/openai/generated/v2/BrowserUseOriginPolicy.ts b/src/runtimes/openai/generated/v2/BrowserUseOriginPolicy.ts new file mode 100644 index 0000000..9614c39 --- /dev/null +++ b/src/runtimes/openai/generated/v2/BrowserUseOriginPolicy.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; +import type { BrowserUseAccessApprovalLifetime } from "./BrowserUseAccessApprovalLifetime"; + +export type BrowserUseOriginPolicy = { + access: AllowDenyRequirement | null; + downloads: AllowDenyRequirement | null; + uploads: AllowDenyRequirement | null; + fullCdpAccess: AllowDenyRequirement | null; + autoReview: AllowDenyRequirement | null; + persistentApproval: boolean | null; + accessApprovalLifetime: BrowserUseAccessApprovalLifetime | null; +}; diff --git a/src/runtimes/openai/generated/v2/BrowserUseOriginPolicyConfig.ts b/src/runtimes/openai/generated/v2/BrowserUseOriginPolicyConfig.ts new file mode 100644 index 0000000..5dce8f1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/BrowserUseOriginPolicyConfig.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; + +export type BrowserUseOriginPolicyConfig = { + access: AllowDenyRequirement | null; + downloads: AllowDenyRequirement | null; + uploads: AllowDenyRequirement | null; + full_cdp_access: AllowDenyRequirement | null; +}; diff --git a/src/runtimes/openai/generated/v2/BrowserUseRequirements.ts b/src/runtimes/openai/generated/v2/BrowserUseRequirements.ts new file mode 100644 index 0000000..7fa1bd9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/BrowserUseRequirements.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BrowserUseOriginPolicy } from "./BrowserUseOriginPolicy"; + +export type BrowserUseRequirements = { + allowHistoryAccess: boolean | null; + disableAutoReview: boolean | null; + allowGlobalPersistentApproval: boolean | null; + defaultOriginPolicy: BrowserUseOriginPolicy | null; + origins: { [key in string]?: BrowserUseOriginPolicy } | null; +}; diff --git a/src/runtimes/openai/generated/v2/ByteRange.ts b/src/runtimes/openai/generated/v2/ByteRange.ts new file mode 100644 index 0000000..fae7a1c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ByteRange.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ByteRange = { start: number; end: number }; diff --git a/src/runtimes/openai/generated/v2/CancelLoginAccountParams.ts b/src/runtimes/openai/generated/v2/CancelLoginAccountParams.ts new file mode 100644 index 0000000..8096042 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CancelLoginAccountParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CancelLoginAccountParams = { loginId: string }; diff --git a/src/runtimes/openai/generated/v2/CancelLoginAccountResponse.ts b/src/runtimes/openai/generated/v2/CancelLoginAccountResponse.ts new file mode 100644 index 0000000..a207c5e --- /dev/null +++ b/src/runtimes/openai/generated/v2/CancelLoginAccountResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CancelLoginAccountStatus } from "./CancelLoginAccountStatus"; + +export type CancelLoginAccountResponse = { status: CancelLoginAccountStatus }; diff --git a/src/runtimes/openai/generated/v2/CancelLoginAccountStatus.ts b/src/runtimes/openai/generated/v2/CancelLoginAccountStatus.ts new file mode 100644 index 0000000..bd851c6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CancelLoginAccountStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CancelLoginAccountStatus = "canceled" | "notFound"; diff --git a/src/runtimes/openai/generated/v2/CapabilityRootLocation.ts b/src/runtimes/openai/generated/v2/CapabilityRootLocation.ts new file mode 100644 index 0000000..a8bb28b --- /dev/null +++ b/src/runtimes/openai/generated/v2/CapabilityRootLocation.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Location used to resolve a selected capability root. + */ +export type CapabilityRootLocation = { + type: "environment"; + environmentId: string; + /** + * Absolute path for the root in the selected environment. + */ + path: string; +}; diff --git a/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshParams.ts b/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshParams.ts new file mode 100644 index 0000000..092838e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshParams.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ChatgptAuthTokensRefreshReason } from "./ChatgptAuthTokensRefreshReason"; + +export type ChatgptAuthTokensRefreshParams = { + reason: ChatgptAuthTokensRefreshReason; + /** + * Workspace/account identifier that Codex was previously using. + * + * Clients that manage multiple accounts/workspaces can use this as a hint + * to refresh the token for the correct workspace. + * + * This may be `null` when the prior auth state did not include a workspace + * identifier (`chatgpt_account_id`). + */ + previousAccountId?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshReason.ts b/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshReason.ts new file mode 100644 index 0000000..ac4006b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshReason.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ChatgptAuthTokensRefreshReason = "unauthorized"; diff --git a/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshResponse.ts b/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshResponse.ts new file mode 100644 index 0000000..0605988 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ChatgptAuthTokensRefreshResponse = { + accessToken: string; + chatgptAccountId: string; + chatgptPlanType: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/CliAuthCredentialsStoreMode.ts b/src/runtimes/openai/generated/v2/CliAuthCredentialsStoreMode.ts new file mode 100644 index 0000000..366d748 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CliAuthCredentialsStoreMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CliAuthCredentialsStoreMode = "file" | "keyring" | "auto" | "ephemeral"; diff --git a/src/runtimes/openai/generated/v2/CodexErrorInfo.ts b/src/runtimes/openai/generated/v2/CodexErrorInfo.ts new file mode 100644 index 0000000..549d101 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CodexErrorInfo.ts @@ -0,0 +1,29 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NonSteerableTurnKind } from "./NonSteerableTurnKind"; + +/** + * This translation layer make sure that we expose codex error code in camel case. + * + * When an upstream HTTP status is available (for example, from the Responses API or a provider), + * it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant. + */ +export type CodexErrorInfo = + | "contextWindowExceeded" + | "sessionBudgetExceeded" + | "usageLimitExceeded" + | "serverOverloaded" + | "cyberPolicy" + | "misalignmentPolicyViolation" + | { httpConnectionFailed: { httpStatusCode: number | null } } + | { responseStreamConnectionFailed: { httpStatusCode: number | null } } + | "internalServerError" + | "unauthorized" + | "badRequest" + | "threadRollbackFailed" + | "sandboxError" + | { responseStreamDisconnected: { httpStatusCode: number | null } } + | { responseTooManyFailedAttempts: { httpStatusCode: number | null } } + | { activeTurnNotSteerable: { turnKind: NonSteerableTurnKind } } + | "other"; diff --git a/src/runtimes/openai/generated/v2/CollabAgentState.ts b/src/runtimes/openai/generated/v2/CollabAgentState.ts new file mode 100644 index 0000000..0bfff00 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CollabAgentState.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CollabAgentStatus } from "./CollabAgentStatus"; + +export type CollabAgentState = { status: CollabAgentStatus; message: string | null }; diff --git a/src/runtimes/openai/generated/v2/CollabAgentStatus.ts b/src/runtimes/openai/generated/v2/CollabAgentStatus.ts new file mode 100644 index 0000000..5bff8e1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CollabAgentStatus.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CollabAgentStatus = + | "pendingInit" + | "running" + | "interrupted" + | "completed" + | "errored" + | "shutdown" + | "notFound"; diff --git a/src/runtimes/openai/generated/v2/CollabAgentTool.ts b/src/runtimes/openai/generated/v2/CollabAgentTool.ts new file mode 100644 index 0000000..9ce62ac --- /dev/null +++ b/src/runtimes/openai/generated/v2/CollabAgentTool.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CollabAgentTool = + | "spawnAgent" + | "sendInput" + | "resumeAgent" + | "wait" + | "closeAgent" + | "sendMessage" + | "followupTask" + | "interruptAgent" + | "listAgents"; diff --git a/src/runtimes/openai/generated/v2/CollabAgentToolCallStatus.ts b/src/runtimes/openai/generated/v2/CollabAgentToolCallStatus.ts new file mode 100644 index 0000000..2309bb8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CollabAgentToolCallStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CollabAgentToolCallStatus = "inProgress" | "completed" | "failed" | "interrupted"; diff --git a/src/runtimes/openai/generated/v2/CollaborationModeListParams.ts b/src/runtimes/openai/generated/v2/CollaborationModeListParams.ts new file mode 100644 index 0000000..37e8f79 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CollaborationModeListParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - list collaboration mode presets. + */ +export type CollaborationModeListParams = Record; diff --git a/src/runtimes/openai/generated/v2/CollaborationModeListResponse.ts b/src/runtimes/openai/generated/v2/CollaborationModeListResponse.ts new file mode 100644 index 0000000..675f02d --- /dev/null +++ b/src/runtimes/openai/generated/v2/CollaborationModeListResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CollaborationModeMask } from "./CollaborationModeMask"; + +/** + * EXPERIMENTAL - collaboration mode presets response. + */ +export type CollaborationModeListResponse = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/CollaborationModeMask.ts b/src/runtimes/openai/generated/v2/CollaborationModeMask.ts new file mode 100644 index 0000000..0e30e08 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CollaborationModeMask.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ModeKind } from "../ModeKind"; +import type { ReasoningEffort } from "../ReasoningEffort"; + +/** + * EXPERIMENTAL - collaboration mode preset metadata for clients. + */ +export type CollaborationModeMask = { + name: string; + mode: ModeKind | null; + model: string | null; + reasoning_effort: ReasoningEffort | null | null; +}; diff --git a/src/runtimes/openai/generated/v2/CommandAction.ts b/src/runtimes/openai/generated/v2/CommandAction.ts new file mode 100644 index 0000000..b88e953 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandAction.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LegacyAppPathString } from "../LegacyAppPathString"; + +export type CommandAction = + | { type: "read"; command: string; name: string; path: LegacyAppPathString } + | { type: "listFiles"; command: string; path: string | null } + | { type: "search"; command: string; query: string | null; path: string | null } + | { type: "unknown"; command: string }; diff --git a/src/runtimes/openai/generated/v2/CommandExecOutputDeltaNotification.ts b/src/runtimes/openai/generated/v2/CommandExecOutputDeltaNotification.ts new file mode 100644 index 0000000..988ccc9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecOutputDeltaNotification.ts @@ -0,0 +1,31 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CommandExecOutputStream } from "./CommandExecOutputStream"; + +/** + * Base64-encoded output chunk emitted for a streaming `command/exec` request. + * + * These notifications are connection-scoped. If the originating connection + * closes, the server terminates the process. + */ +export type CommandExecOutputDeltaNotification = { + /** + * Client-supplied, connection-scoped `processId` from the original + * `command/exec` request. + */ + processId: string; + /** + * Output stream for this chunk. + */ + stream: CommandExecOutputStream; + /** + * Base64-encoded output bytes. + */ + deltaBase64: string; + /** + * `true` on the final streamed chunk for a stream when `outputBytesCap` + * truncated later output on that stream. + */ + capReached: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecOutputStream.ts b/src/runtimes/openai/generated/v2/CommandExecOutputStream.ts new file mode 100644 index 0000000..a8c5b66 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecOutputStream.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Stream label for `command/exec/outputDelta` notifications. + */ +export type CommandExecOutputStream = "stdout" | "stderr"; diff --git a/src/runtimes/openai/generated/v2/CommandExecParams.ts b/src/runtimes/openai/generated/v2/CommandExecParams.ts new file mode 100644 index 0000000..1f6cebb --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecParams.ts @@ -0,0 +1,106 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; +import type { SandboxPolicy } from "./SandboxPolicy"; + +/** + * Run a standalone command (argv vector) in the server sandbox without + * creating a thread or turn. + * + * The final `command/exec` response is deferred until the process exits and is + * sent only after all `command/exec/outputDelta` notifications for that + * connection have been emitted. + */ +export type CommandExecParams = { + /** + * Command argv vector. Empty arrays are rejected. + */ + command: Array; + /** + * Optional client-supplied, connection-scoped process id. + * + * Required for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up + * `command/exec/write`, `command/exec/resize`, and + * `command/exec/terminate` calls. When omitted, buffered execution gets an + * internal id that is not exposed to the client. + */ + processId?: string | null; + /** + * Enable PTY mode. + * + * This implies `streamStdin` and `streamStdoutStderr`. + */ + tty?: boolean; + /** + * Allow follow-up `command/exec/write` requests to write stdin bytes. + * + * Requires a client-supplied `processId`. + */ + streamStdin?: boolean; + /** + * Stream stdout/stderr via `command/exec/outputDelta` notifications. + * + * Streamed bytes are not duplicated into the final response and require a + * client-supplied `processId`. + */ + streamStdoutStderr?: boolean; + /** + * Optional per-stream stdout/stderr capture cap in bytes. + * + * When omitted, the server default applies. Cannot be combined with + * `disableOutputCap`. + */ + outputBytesCap?: number | null; + /** + * Disable stdout/stderr capture truncation for this request. + * + * Cannot be combined with `outputBytesCap`. + */ + disableOutputCap?: boolean; + /** + * Disable the timeout entirely for this request. + * + * Cannot be combined with `timeoutMs`. + */ + disableTimeout?: boolean; + /** + * Optional timeout in milliseconds. + * + * When omitted, the server default applies. Cannot be combined with + * `disableTimeout`. + */ + timeoutMs?: number | null; + /** + * Optional working directory. Defaults to the server cwd. + */ + cwd?: string | null; + /** + * Optional environment overrides merged into the server-computed + * environment. + * + * Matching names override inherited values. Set a key to `null` to unset + * an inherited variable. + */ + env?: { [key in string]?: string | null } | null; + /** + * Optional initial PTY size in character cells. Only valid when `tty` is + * true. + */ + size?: CommandExecTerminalSize | null; + /** + * Optional sandbox policy for this command. + * + * Uses the same shape as thread/turn execution sandbox configuration and + * defaults to the user's configured policy when omitted. Cannot be + * combined with `permissionProfile`. + */ + sandboxPolicy?: SandboxPolicy | null; + /** + * Optional active permissions profile id for this command. + * + * Defaults to the user's configured permissions when omitted. Cannot be + * combined with `sandboxPolicy`. + */ + permissionProfile?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecResizeParams.ts b/src/runtimes/openai/generated/v2/CommandExecResizeParams.ts new file mode 100644 index 0000000..5a29036 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecResizeParams.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; + +/** + * Resize a running PTY-backed `command/exec` session. + */ +export type CommandExecResizeParams = { + /** + * Client-supplied, connection-scoped `processId` from the original + * `command/exec` request. + */ + processId: string; + /** + * New PTY size in character cells. + */ + size: CommandExecTerminalSize; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecResizeResponse.ts b/src/runtimes/openai/generated/v2/CommandExecResizeResponse.ts new file mode 100644 index 0000000..7b7f2be --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecResizeResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Empty success response for `command/exec/resize`. + */ +export type CommandExecResizeResponse = Record; diff --git a/src/runtimes/openai/generated/v2/CommandExecResponse.ts b/src/runtimes/openai/generated/v2/CommandExecResponse.ts new file mode 100644 index 0000000..caa53bd --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecResponse.ts @@ -0,0 +1,25 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Final buffered result for `command/exec`. + */ +export type CommandExecResponse = { + /** + * Process exit code. + */ + exitCode: number; + /** + * Buffered stdout capture. + * + * Empty when stdout was streamed via `command/exec/outputDelta`. + */ + stdout: string; + /** + * Buffered stderr capture. + * + * Empty when stderr was streamed via `command/exec/outputDelta`. + */ + stderr: string; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecTerminalSize.ts b/src/runtimes/openai/generated/v2/CommandExecTerminalSize.ts new file mode 100644 index 0000000..5d409e0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecTerminalSize.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * PTY size in character cells for `command/exec` PTY sessions. + */ +export type CommandExecTerminalSize = { + /** + * Terminal height in character cells. + */ + rows: number; + /** + * Terminal width in character cells. + */ + cols: number; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecTerminateParams.ts b/src/runtimes/openai/generated/v2/CommandExecTerminateParams.ts new file mode 100644 index 0000000..43cbaab --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecTerminateParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Terminate a running `command/exec` session. + */ +export type CommandExecTerminateParams = { + /** + * Client-supplied, connection-scoped `processId` from the original + * `command/exec` request. + */ + processId: string; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecTerminateResponse.ts b/src/runtimes/openai/generated/v2/CommandExecTerminateResponse.ts new file mode 100644 index 0000000..dc6371f --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecTerminateResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Empty success response for `command/exec/terminate`. + */ +export type CommandExecTerminateResponse = Record; diff --git a/src/runtimes/openai/generated/v2/CommandExecWriteParams.ts b/src/runtimes/openai/generated/v2/CommandExecWriteParams.ts new file mode 100644 index 0000000..960fadc --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecWriteParams.ts @@ -0,0 +1,23 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Write stdin bytes to a running `command/exec` session, close stdin, or + * both. + */ +export type CommandExecWriteParams = { + /** + * Client-supplied, connection-scoped `processId` from the original + * `command/exec` request. + */ + processId: string; + /** + * Optional base64-encoded stdin bytes to write. + */ + deltaBase64?: string | null; + /** + * Close stdin after writing `deltaBase64`, if present. + */ + closeStdin?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecWriteResponse.ts b/src/runtimes/openai/generated/v2/CommandExecWriteResponse.ts new file mode 100644 index 0000000..6dbbddf --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecWriteResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Empty success response for `command/exec/write`. + */ +export type CommandExecWriteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionApprovalDecision.ts b/src/runtimes/openai/generated/v2/CommandExecutionApprovalDecision.ts new file mode 100644 index 0000000..79256f8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecutionApprovalDecision.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; +import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; + +export type CommandExecutionApprovalDecision = + | "accept" + | "acceptForSession" + | { acceptWithExecpolicyAmendment: { execpolicy_amendment: ExecPolicyAmendment } } + | { applyNetworkPolicyAmendment: { network_policy_amendment: NetworkPolicyAmendment } } + | "decline" + | "cancel"; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionApprovalKind.ts b/src/runtimes/openai/generated/v2/CommandExecutionApprovalKind.ts new file mode 100644 index 0000000..f94eac8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecutionApprovalKind.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Distinguishes a command approval from input sent to an existing terminal. + */ +export type CommandExecutionApprovalKind = "command" | "writeStdin"; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionOutputDeltaNotification.ts b/src/runtimes/openai/generated/v2/CommandExecutionOutputDeltaNotification.ts new file mode 100644 index 0000000..c476d44 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecutionOutputDeltaNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CommandExecutionOutputDeltaNotification = { + threadId: string; + turnId: string; + itemId: string; + delta: string; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalParams.ts b/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalParams.ts new file mode 100644 index 0000000..836d948 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalParams.ts @@ -0,0 +1,76 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LegacyAppPathString } from "../LegacyAppPathString"; +import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; +import type { CommandAction } from "./CommandAction"; +import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; +import type { CommandExecutionApprovalKind } from "./CommandExecutionApprovalKind"; +import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; +import type { NetworkApprovalContext } from "./NetworkApprovalContext"; +import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; + +export type CommandExecutionRequestApprovalParams = { + /** + * Kind of action under review. Defaults to `command` for older servers. + */ + kind: CommandExecutionApprovalKind; + threadId: string; + turnId: string; + itemId: string; + /** + * Unix timestamp (in milliseconds) when this approval request started. + */ + startedAtMs: number; + /** + * Unique identifier for this specific approval callback. + * + * For regular shell/unified_exec approvals, this is null. + * + * For zsh-exec-bridge subcommand approvals, multiple callbacks can belong to + * one parent `itemId`, so `approvalId` is a distinct opaque callback id + * (a UUID) used to disambiguate routing. + * Stdin approvals also use a distinct callback id; inspect `kind` to distinguish them. + */ + approvalId?: string | null; + /** + * Environment in which the command will run. + */ + environmentId: string | null; + /** + * Optional explanatory reason (e.g. request for network access). + */ + reason?: string | null; + /** + * Optional context for a managed-network approval prompt. + */ + networkApprovalContext?: NetworkApprovalContext | null; + /** + * The command to be executed. + */ + command?: string | null; + /** + * The command's working directory. + */ + cwd?: LegacyAppPathString | null; + /** + * Best-effort parsed command actions for friendly display. + */ + commandActions?: Array | null; + /** + * Optional additional permissions requested for this command. + */ + additionalPermissions?: AdditionalPermissionProfile | null; + /** + * Optional proposed execpolicy amendment to allow similar commands without prompting. + */ + proposedExecpolicyAmendment?: ExecPolicyAmendment | null; + /** + * Optional proposed network policy amendments (allow/deny host) for future requests. + */ + proposedNetworkPolicyAmendments?: Array | null; + /** + * Ordered list of decisions the client may present for this prompt. + */ + availableDecisions?: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalResponse.ts b/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalResponse.ts new file mode 100644 index 0000000..b4b31d0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; + +export type CommandExecutionRequestApprovalResponse = { + decision: CommandExecutionApprovalDecision; +}; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionSource.ts b/src/runtimes/openai/generated/v2/CommandExecutionSource.ts new file mode 100644 index 0000000..790b65b --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecutionSource.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CommandExecutionSource = + | "agent" + | "userShell" + | "unifiedExecStartup" + | "unifiedExecInteraction"; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionStatus.ts b/src/runtimes/openai/generated/v2/CommandExecutionStatus.ts new file mode 100644 index 0000000..c58b3cc --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandExecutionStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CommandExecutionStatus = "inProgress" | "completed" | "failed" | "declined"; diff --git a/src/runtimes/openai/generated/v2/CommandMigration.ts b/src/runtimes/openai/generated/v2/CommandMigration.ts new file mode 100644 index 0000000..8f2bee0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CommandMigration.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CommandMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/ComputerUseConfig.ts b/src/runtimes/openai/generated/v2/ComputerUseConfig.ts new file mode 100644 index 0000000..266f935 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ComputerUseConfig.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; +import type { ComputerUseMacosConfig } from "./ComputerUseMacosConfig"; +import type { ComputerUseWindowsConfig } from "./ComputerUseWindowsConfig"; + +export type ComputerUseConfig = { + default_app_access: AllowDenyRequirement | null; + macos: ComputerUseMacosConfig | null; + windows: ComputerUseWindowsConfig | null; +}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseMacosConfig.ts b/src/runtimes/openai/generated/v2/ComputerUseMacosConfig.ts new file mode 100644 index 0000000..cfbf6d3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ComputerUseMacosConfig.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; + +export type ComputerUseMacosConfig = { + bundle_ids: { [key in string]?: AllowDenyRequirement } | null; +}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseMacosRequirements.ts b/src/runtimes/openai/generated/v2/ComputerUseMacosRequirements.ts new file mode 100644 index 0000000..6b329e2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ComputerUseMacosRequirements.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; + +export type ComputerUseMacosRequirements = { + bundleIds: { [key in string]?: AllowDenyRequirement } | null; +}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseRequirements.ts b/src/runtimes/openai/generated/v2/ComputerUseRequirements.ts new file mode 100644 index 0000000..bf7d9bd --- /dev/null +++ b/src/runtimes/openai/generated/v2/ComputerUseRequirements.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; +import type { ComputerUseMacosRequirements } from "./ComputerUseMacosRequirements"; +import type { ComputerUseWindowsRequirements } from "./ComputerUseWindowsRequirements"; + +export type ComputerUseRequirements = { + allowLockedComputerUse: boolean | null; + allowPersistentApproval: boolean | null; + defaultAppAccess: AllowDenyRequirement | null; + macos: ComputerUseMacosRequirements | null; + windows: ComputerUseWindowsRequirements | null; +}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseWindowsConfig.ts b/src/runtimes/openai/generated/v2/ComputerUseWindowsConfig.ts new file mode 100644 index 0000000..f07511c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ComputerUseWindowsConfig.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; +import type { ComputerUseWindowsExeConfig } from "./ComputerUseWindowsExeConfig"; + +export type ComputerUseWindowsConfig = { + aumids: { [key in string]?: AllowDenyRequirement } | null; + exes: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseWindowsExeConfig.ts b/src/runtimes/openai/generated/v2/ComputerUseWindowsExeConfig.ts new file mode 100644 index 0000000..00569e6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ComputerUseWindowsExeConfig.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; + +export type ComputerUseWindowsExeConfig = { + publisher_name: string; + product_name: string; + binary_name: string | null; + access: AllowDenyRequirement; +}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseWindowsExeRequirement.ts b/src/runtimes/openai/generated/v2/ComputerUseWindowsExeRequirement.ts new file mode 100644 index 0000000..191fe73 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ComputerUseWindowsExeRequirement.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; + +export type ComputerUseWindowsExeRequirement = { + publisherName: string; + productName: string; + binaryName: string | null; + access: AllowDenyRequirement; +}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseWindowsRequirements.ts b/src/runtimes/openai/generated/v2/ComputerUseWindowsRequirements.ts new file mode 100644 index 0000000..9ce7c33 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ComputerUseWindowsRequirements.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowDenyRequirement } from "./AllowDenyRequirement"; +import type { ComputerUseWindowsExeRequirement } from "./ComputerUseWindowsExeRequirement"; + +export type ComputerUseWindowsRequirements = { + aumids: { [key in string]?: AllowDenyRequirement } | null; + exes: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/Config.ts b/src/runtimes/openai/generated/v2/Config.ts new file mode 100644 index 0000000..5fd1848 --- /dev/null +++ b/src/runtimes/openai/generated/v2/Config.ts @@ -0,0 +1,61 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AutoCompactTokenLimitScope } from "../AutoCompactTokenLimitScope"; +import type { ForcedLoginMethod } from "../ForcedLoginMethod"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ReasoningSummary } from "../ReasoningSummary"; +import type { Verbosity } from "../Verbosity"; +import type { WebSearchMode } from "../WebSearchMode"; +import type { JsonValue } from "../serde_json/JsonValue"; +import type { AnalyticsConfig } from "./AnalyticsConfig"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AppsConfig } from "./AppsConfig"; +import type { AskForApproval } from "./AskForApproval"; +import type { BrowserUseConfig } from "./BrowserUseConfig"; +import type { ComputerUseConfig } from "./ComputerUseConfig"; +import type { ForcedChatgptWorkspaceIds } from "./ForcedChatgptWorkspaceIds"; +import type { SandboxMode } from "./SandboxMode"; +import type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; +import type { ToolsV2 } from "./ToolsV2"; + +export type Config = { + model: string | null; + review_model: string | null; + model_context_window: bigint | null; + model_auto_compact_token_limit: bigint | null; + model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope | null; + model_provider: string | null; + approval_policy: AskForApproval | null; + /** + * [UNSTABLE] Optional default for where approval requests are routed for + * review. + */ + approvals_reviewer: ApprovalsReviewer | null; + sandbox_mode: SandboxMode | null; + sandbox_workspace_write: SandboxWorkspaceWrite | null; + forced_chatgpt_workspace_id: ForcedChatgptWorkspaceIds | null; + forced_login_method: ForcedLoginMethod | null; + web_search: WebSearchMode | null; + tools: ToolsV2 | null; + instructions: string | null; + developer_instructions: string | null; + compact_prompt: string | null; + model_reasoning_effort: ReasoningEffort | null; + model_reasoning_summary: ReasoningSummary | null; + model_verbosity: Verbosity | null; + service_tier: string | null; + analytics: AnalyticsConfig | null; + apps: AppsConfig | null; + browser_use: BrowserUseConfig | null; + computer_use: ComputerUseConfig | null; + desktop: { [key in string]?: JsonValue } | null; +} & { + [key in string]?: + | number + | string + | boolean + | Array + | { [key in string]?: JsonValue } + | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConfigBatchWriteParams.ts b/src/runtimes/openai/generated/v2/ConfigBatchWriteParams.ts new file mode 100644 index 0000000..91eff21 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigBatchWriteParams.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigEdit } from "./ConfigEdit"; + +export type ConfigBatchWriteParams = { + edits: Array; + /** + * Path to the config file to write; defaults to the user's `config.toml` when omitted. + */ + filePath?: string | null; + expectedVersion?: string | null; + /** + * When true, hot-reload updated runtime settings into loaded threads after writing. + * Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and + * personality defaults are not reloaded. + */ + reloadUserConfig?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ConfigEdit.ts b/src/runtimes/openai/generated/v2/ConfigEdit.ts new file mode 100644 index 0000000..b13e75b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigEdit.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; +import type { MergeStrategy } from "./MergeStrategy"; + +export type ConfigEdit = { keyPath: string; value: JsonValue; mergeStrategy: MergeStrategy }; diff --git a/src/runtimes/openai/generated/v2/ConfigLayer.ts b/src/runtimes/openai/generated/v2/ConfigLayer.ts new file mode 100644 index 0000000..e549226 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigLayer.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; +import type { ConfigLayerSource } from "./ConfigLayerSource"; + +export type ConfigLayer = { + name: ConfigLayerSource; + version: string; + config: JsonValue; + disabledReason: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConfigLayerMetadata.ts b/src/runtimes/openai/generated/v2/ConfigLayerMetadata.ts new file mode 100644 index 0000000..989a5a6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigLayerMetadata.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigLayerSource } from "./ConfigLayerSource"; + +export type ConfigLayerMetadata = { name: ConfigLayerSource; version: string }; diff --git a/src/runtimes/openai/generated/v2/ConfigLayerSource.ts b/src/runtimes/openai/generated/v2/ConfigLayerSource.ts new file mode 100644 index 0000000..4c1b296 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigLayerSource.ts @@ -0,0 +1,52 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type ConfigLayerSource = + | { + type: "packagedDefaults"; + /** + * Path to the packaged default configuration file. + */ + file: AbsolutePathBuf; + } + | { type: "mdm"; domain: string; key: string } + | { + type: "system"; + /** + * This is the path to the system config.toml file, though it is not + * guaranteed to exist. + */ + file: AbsolutePathBuf; + } + | { + type: "enterpriseManaged"; + /** + * Stable identifier for the delivered layer. + */ + id: string; + /** + * Admin-facing name for the delivered layer. This is surfaced in + * diagnostics so users know which cloud layer needs administrator + * attention. + */ + name: string; + } + | { + type: "user"; + /** + * This is the path to the user's config.toml file, though it is not + * guaranteed to exist. + */ + file: AbsolutePathBuf; + /** + * Name of the selected profile-v2 config layered on top of the base + * user config, when this layer represents one. + */ + profile: string | null; + } + | { type: "project"; dotCodexFolder: AbsolutePathBuf } + | { type: "sessionFlags" } + | { type: "legacyManagedConfigTomlFromFile"; file: AbsolutePathBuf } + | { type: "legacyManagedConfigTomlFromMdm" }; diff --git a/src/runtimes/openai/generated/v2/ConfigReadParams.ts b/src/runtimes/openai/generated/v2/ConfigReadParams.ts new file mode 100644 index 0000000..14de081 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigReadParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ConfigReadParams = { + includeLayers?: boolean; + /** + * Optional working directory to resolve project config layers. If specified, + * return the effective config as seen from that directory (i.e., including any + * project layers between `cwd` and the project/repo root). + */ + cwd?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConfigReadResponse.ts b/src/runtimes/openai/generated/v2/ConfigReadResponse.ts new file mode 100644 index 0000000..fcd4a88 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigReadResponse.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Config } from "./Config"; +import type { ConfigLayer } from "./ConfigLayer"; +import type { ConfigLayerMetadata } from "./ConfigLayerMetadata"; + +export type ConfigReadResponse = { + config: Config; + origins: { [key in string]?: ConfigLayerMetadata }; + layers: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConfigRequirements.ts b/src/runtimes/openai/generated/v2/ConfigRequirements.ts new file mode 100644 index 0000000..9faeb64 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigRequirements.ts @@ -0,0 +1,52 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PathUri } from "../PathUri"; +import type { WebSearchMode } from "../WebSearchMode"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { AutoReviewRequirements } from "./AutoReviewRequirements"; +import type { BrowserUseRequirements } from "./BrowserUseRequirements"; +import type { CliAuthCredentialsStoreMode } from "./CliAuthCredentialsStoreMode"; +import type { ComputerUseRequirements } from "./ComputerUseRequirements"; +import type { FeedbackRequirements } from "./FeedbackRequirements"; +import type { InAppBrowserRequirements } from "./InAppBrowserRequirements"; +import type { ManagedHooksRequirements } from "./ManagedHooksRequirements"; +import type { ModelsRequirements } from "./ModelsRequirements"; +import type { NetworkRequirements } from "./NetworkRequirements"; +import type { ResidencyRequirement } from "./ResidencyRequirement"; +import type { SandboxMode } from "./SandboxMode"; +import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; + +export type ConfigRequirements = { + cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null; + chatgptBaseUrl: string | null; + additionalDeveloperInstructions: string | null; + allowedApprovalPolicies: Array | null; + allowedApprovalsReviewers: Array | null; + allowedSandboxModes: Array | null; + allowedWindowsSandboxImplementations: Array | null; + allowedPermissionProfiles: { [key in string]?: boolean } | null; + defaultPermissions: string | null; + allowedWebSearchModes: Array | null; + allowManagedHooksOnly: boolean | null; + allowBrowserAndComputerUse: boolean | null; + allowAppshots: boolean | null; + allowRemoteControl: boolean | null; + computerUse: ComputerUseRequirements | null; + browserUse: BrowserUseRequirements | null; + inAppBrowser: InAppBrowserRequirements | null; + featureRequirements: { [key in string]?: boolean } | null; + hooks: ManagedHooksRequirements | null; + enforceResidency: ResidencyRequirement | null; + network: NetworkRequirements | null; + autoReview: AutoReviewRequirements | null; + models: ModelsRequirements | null; + sqliteHome: PathUri | null; + logDir: PathUri | null; + modelCatalogJson: PathUri | null; + checkForUpdateOnStartup: boolean | null; + allowLoginShell: boolean | null; + feedback: FeedbackRequirements | null; + windowsSandboxPrivateDesktop: boolean | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConfigRequirementsReadResponse.ts b/src/runtimes/openai/generated/v2/ConfigRequirementsReadResponse.ts new file mode 100644 index 0000000..14e736c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigRequirementsReadResponse.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigRequirements } from "./ConfigRequirements"; + +export type ConfigRequirementsReadResponse = { + /** + * Null if no requirements are configured (e.g. no requirements.toml/MDM entries). + */ + requirements: ConfigRequirements | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConfigValueWriteParams.ts b/src/runtimes/openai/generated/v2/ConfigValueWriteParams.ts new file mode 100644 index 0000000..34cd296 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigValueWriteParams.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; +import type { MergeStrategy } from "./MergeStrategy"; + +export type ConfigValueWriteParams = { + keyPath: string; + value: JsonValue; + mergeStrategy: MergeStrategy; + /** + * Path to the config file to write; defaults to the user's `config.toml` when omitted. + */ + filePath?: string | null; + expectedVersion?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConfigWarningNotification.ts b/src/runtimes/openai/generated/v2/ConfigWarningNotification.ts new file mode 100644 index 0000000..8efae84 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigWarningNotification.ts @@ -0,0 +1,23 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TextRange } from "./TextRange"; + +export type ConfigWarningNotification = { + /** + * Concise summary of the warning. + */ + summary: string; + /** + * Optional extra guidance or error details. + */ + details: string | null; + /** + * Optional path to the config file that triggered the warning. + */ + path?: string; + /** + * Optional range for the error location inside the config file. + */ + range?: TextRange; +}; diff --git a/src/runtimes/openai/generated/v2/ConfigWriteResponse.ts b/src/runtimes/openai/generated/v2/ConfigWriteResponse.ts new file mode 100644 index 0000000..7c54ebc --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfigWriteResponse.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { OverriddenMetadata } from "./OverriddenMetadata"; +import type { WriteStatus } from "./WriteStatus"; + +export type ConfigWriteResponse = { + status: WriteStatus; + version: string; + /** + * Canonical path to the config file that was written. + */ + filePath: AbsolutePathBuf; + overriddenMetadata: OverriddenMetadata | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConfiguredHookHandler.ts b/src/runtimes/openai/generated/v2/ConfiguredHookHandler.ts new file mode 100644 index 0000000..76e9468 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfiguredHookHandler.ts @@ -0,0 +1,31 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type ConfiguredHookHandler = + | { + type: "command"; + command: string; + commandWindows: string | null; + timeoutSec: bigint | null; + async: boolean; + statusMessage: string | null; + /** + * Approximate token threshold for spilling this hook's `additionalContext` to disk. + * `null` uses 2,500 tokens; `0` disables spilling for this hook. The threshold is + * evaluated against the original context; a spilled preview also includes recovery + * metadata. + */ + additionalContextLimit: number | null; + } + | { + type: "mcp_tool"; + server: string; + tool: string; + input: { [key in string]?: JsonValue }; + timeoutSec: bigint | null; + statusMessage: string | null; + } + | { type: "prompt" } + | { type: "agent" }; diff --git a/src/runtimes/openai/generated/v2/ConfiguredHookMatcherGroup.ts b/src/runtimes/openai/generated/v2/ConfiguredHookMatcherGroup.ts new file mode 100644 index 0000000..2da0a76 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConfiguredHookMatcherGroup.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfiguredHookHandler } from "./ConfiguredHookHandler"; + +export type ConfiguredHookMatcherGroup = { + matcher: string | null; + hooks: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ConnectorMetadata.ts b/src/runtimes/openai/generated/v2/ConnectorMetadata.ts new file mode 100644 index 0000000..9b324f7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConnectorMetadata.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppToolSummary } from "./AppToolSummary"; + +/** + * EXPERIMENTAL - metadata returned by app/read. + */ +export type ConnectorMetadata = { + id: string; + name: string; + description: string | null; + iconUrl: string | null; + iconUrlDark: string | null; + distributionChannel: string | null; + installUrl: string | null; + pluginDisplayNames: Array; + toolSummaries: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditOutcome.ts b/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditOutcome.ts new file mode 100644 index 0000000..bfe843e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditOutcome.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ConsumeAccountRateLimitResetCreditOutcome = + | "reset" + | "nothingToReset" + | "noCredit" + | "alreadyRedeemed"; diff --git a/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditParams.ts b/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditParams.ts new file mode 100644 index 0000000..2f52827 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditParams.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ConsumeAccountRateLimitResetCreditParams = { + /** + * Identifies one logical reset attempt. A UUID is recommended; reuse the same value when + * retrying that attempt. + */ + idempotencyKey: string; + /** + * Opaque reset-credit identifier to redeem. When omitted, the backend selects the next + * available credit. + */ + creditId?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditResponse.ts b/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditResponse.ts new file mode 100644 index 0000000..455448e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConsumeAccountRateLimitResetCreditOutcome } from "./ConsumeAccountRateLimitResetCreditOutcome"; + +export type ConsumeAccountRateLimitResetCreditResponse = { + outcome: ConsumeAccountRateLimitResetCreditOutcome; +}; diff --git a/src/runtimes/openai/generated/v2/ContextCompactedNotification.ts b/src/runtimes/openai/generated/v2/ContextCompactedNotification.ts new file mode 100644 index 0000000..bb68253 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ContextCompactedNotification.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Deprecated: Use `ContextCompaction` item type instead. + */ +export type ContextCompactedNotification = { threadId: string; turnId: string }; diff --git a/src/runtimes/openai/generated/v2/CreditsSnapshot.ts b/src/runtimes/openai/generated/v2/CreditsSnapshot.ts new file mode 100644 index 0000000..dd5e746 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CreditsSnapshot.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CreditsSnapshot = { hasCredits: boolean; unlimited: boolean; balance: string | null }; diff --git a/src/runtimes/openai/generated/v2/CurrentTimeReadParams.ts b/src/runtimes/openai/generated/v2/CurrentTimeReadParams.ts new file mode 100644 index 0000000..3c8c459 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CurrentTimeReadParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CurrentTimeReadParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/CurrentTimeReadResponse.ts b/src/runtimes/openai/generated/v2/CurrentTimeReadResponse.ts new file mode 100644 index 0000000..fc991d7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CurrentTimeReadResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CurrentTimeReadResponse = { + /** + * Current time as whole Unix seconds. + */ + currentTimeAt: number; +}; diff --git a/src/runtimes/openai/generated/v2/DeprecationNoticeNotification.ts b/src/runtimes/openai/generated/v2/DeprecationNoticeNotification.ts new file mode 100644 index 0000000..54f3d08 --- /dev/null +++ b/src/runtimes/openai/generated/v2/DeprecationNoticeNotification.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DeprecationNoticeNotification = { + /** + * Concise summary of what is deprecated. + */ + summary: string; + /** + * Optional extra guidance, such as migration steps or rationale. + */ + details: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/DesktopOnboardingEntrypoint.ts b/src/runtimes/openai/generated/v2/DesktopOnboardingEntrypoint.ts new file mode 100644 index 0000000..75a7321 --- /dev/null +++ b/src/runtimes/openai/generated/v2/DesktopOnboardingEntrypoint.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DesktopOnboardingEntrypoint = "life_sciences"; diff --git a/src/runtimes/openai/generated/v2/DynamicToolCallOutputContentItem.ts b/src/runtimes/openai/generated/v2/DynamicToolCallOutputContentItem.ts new file mode 100644 index 0000000..16f6769 --- /dev/null +++ b/src/runtimes/openai/generated/v2/DynamicToolCallOutputContentItem.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DynamicToolCallOutputContentItem = + | { type: "inputText"; text: string } + | { type: "inputImage"; imageUrl: string } + | { type: "inputAudio"; audioUrl: string }; diff --git a/src/runtimes/openai/generated/v2/DynamicToolCallParams.ts b/src/runtimes/openai/generated/v2/DynamicToolCallParams.ts new file mode 100644 index 0000000..76d89b5 --- /dev/null +++ b/src/runtimes/openai/generated/v2/DynamicToolCallParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type DynamicToolCallParams = { + threadId: string; + turnId: string; + callId: string; + namespace: string | null; + tool: string; + arguments: JsonValue; +}; diff --git a/src/runtimes/openai/generated/v2/DynamicToolCallResponse.ts b/src/runtimes/openai/generated/v2/DynamicToolCallResponse.ts new file mode 100644 index 0000000..51b6d6c --- /dev/null +++ b/src/runtimes/openai/generated/v2/DynamicToolCallResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; + +export type DynamicToolCallResponse = { + contentItems: Array; + success: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/DynamicToolCallStatus.ts b/src/runtimes/openai/generated/v2/DynamicToolCallStatus.ts new file mode 100644 index 0000000..04f44ec --- /dev/null +++ b/src/runtimes/openai/generated/v2/DynamicToolCallStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DynamicToolCallStatus = "inProgress" | "completed" | "failed"; diff --git a/src/runtimes/openai/generated/v2/DynamicToolFunctionSpec.ts b/src/runtimes/openai/generated/v2/DynamicToolFunctionSpec.ts new file mode 100644 index 0000000..5769370 --- /dev/null +++ b/src/runtimes/openai/generated/v2/DynamicToolFunctionSpec.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type DynamicToolFunctionSpec = { + name: string; + description: string; + inputSchema: JsonValue; + deferLoading?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/DynamicToolNamespaceSpec.ts b/src/runtimes/openai/generated/v2/DynamicToolNamespaceSpec.ts new file mode 100644 index 0000000..5f55f30 --- /dev/null +++ b/src/runtimes/openai/generated/v2/DynamicToolNamespaceSpec.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DynamicToolNamespaceTool } from "./DynamicToolNamespaceTool"; + +export type DynamicToolNamespaceSpec = { + name: string; + description: string; + tools: Array; +}; diff --git a/src/runtimes/openai/generated/v2/DynamicToolNamespaceTool.ts b/src/runtimes/openai/generated/v2/DynamicToolNamespaceTool.ts new file mode 100644 index 0000000..c1b6d4d --- /dev/null +++ b/src/runtimes/openai/generated/v2/DynamicToolNamespaceTool.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DynamicToolFunctionSpec } from "./DynamicToolFunctionSpec"; + +export type DynamicToolNamespaceTool = { type: "function" } & DynamicToolFunctionSpec; diff --git a/src/runtimes/openai/generated/v2/DynamicToolSpec.ts b/src/runtimes/openai/generated/v2/DynamicToolSpec.ts new file mode 100644 index 0000000..429bb3f --- /dev/null +++ b/src/runtimes/openai/generated/v2/DynamicToolSpec.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DynamicToolFunctionSpec } from "./DynamicToolFunctionSpec"; +import type { DynamicToolNamespaceSpec } from "./DynamicToolNamespaceSpec"; + +export type DynamicToolSpec = + | ({ type: "function" } & DynamicToolFunctionSpec) + | ({ type: "namespace" } & DynamicToolNamespaceSpec); diff --git a/src/runtimes/openai/generated/v2/EnvironmentAddParams.ts b/src/runtimes/openai/generated/v2/EnvironmentAddParams.ts new file mode 100644 index 0000000..005fdc9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/EnvironmentAddParams.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type EnvironmentAddParams = { + environmentId: string; + execServerUrl: string; + /** + * Optional WebSocket connection timeout. The server default applies when omitted. + */ + connectTimeoutMs?: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/EnvironmentAddResponse.ts b/src/runtimes/openai/generated/v2/EnvironmentAddResponse.ts new file mode 100644 index 0000000..5b0a2da --- /dev/null +++ b/src/runtimes/openai/generated/v2/EnvironmentAddResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type EnvironmentAddResponse = Record; diff --git a/src/runtimes/openai/generated/v2/EnvironmentConnectionNotification.ts b/src/runtimes/openai/generated/v2/EnvironmentConnectionNotification.ts new file mode 100644 index 0000000..1c62da1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/EnvironmentConnectionNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type EnvironmentConnectionNotification = { threadId: string; environmentId: string }; diff --git a/src/runtimes/openai/generated/v2/EnvironmentInfoParams.ts b/src/runtimes/openai/generated/v2/EnvironmentInfoParams.ts new file mode 100644 index 0000000..e52a6fd --- /dev/null +++ b/src/runtimes/openai/generated/v2/EnvironmentInfoParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type EnvironmentInfoParams = { environmentId: string }; diff --git a/src/runtimes/openai/generated/v2/EnvironmentInfoResponse.ts b/src/runtimes/openai/generated/v2/EnvironmentInfoResponse.ts new file mode 100644 index 0000000..f830eae --- /dev/null +++ b/src/runtimes/openai/generated/v2/EnvironmentInfoResponse.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PathUri } from "../PathUri"; +import type { EnvironmentShellInfo } from "./EnvironmentShellInfo"; + +export type EnvironmentInfoResponse = { + shell: EnvironmentShellInfo; + /** + * Default working directory reported by the environment, as a canonical file URI. + */ + cwd: PathUri | null; +}; diff --git a/src/runtimes/openai/generated/v2/EnvironmentShellInfo.ts b/src/runtimes/openai/generated/v2/EnvironmentShellInfo.ts new file mode 100644 index 0000000..b89a421 --- /dev/null +++ b/src/runtimes/openai/generated/v2/EnvironmentShellInfo.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type EnvironmentShellInfo = { + /** + * Stable shell name, for example `zsh`, `bash`, `powershell`, `sh`, or `cmd`. + */ + name: string; + /** + * Target-native shell executable path or command name. + */ + path: string; +}; diff --git a/src/runtimes/openai/generated/v2/EnvironmentStatusKind.ts b/src/runtimes/openai/generated/v2/EnvironmentStatusKind.ts new file mode 100644 index 0000000..cc535a2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/EnvironmentStatusKind.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Current status observed by app-server without starting or recovering an environment. + * + * For a currently ready remote environment, app-server asks the existing + * exec-server connection for `environment/status` without allowing recovery. + */ +export type EnvironmentStatusKind = "ready" | "pending" | "disconnected" | "unknown"; diff --git a/src/runtimes/openai/generated/v2/EnvironmentStatusParams.ts b/src/runtimes/openai/generated/v2/EnvironmentStatusParams.ts new file mode 100644 index 0000000..9d3b500 --- /dev/null +++ b/src/runtimes/openai/generated/v2/EnvironmentStatusParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Parameters for reading the current status of one configured environment. + */ +export type EnvironmentStatusParams = { + /** + * Environment id to inspect. + */ + environmentId: string; +}; diff --git a/src/runtimes/openai/generated/v2/EnvironmentStatusResponse.ts b/src/runtimes/openai/generated/v2/EnvironmentStatusResponse.ts new file mode 100644 index 0000000..fd7251a --- /dev/null +++ b/src/runtimes/openai/generated/v2/EnvironmentStatusResponse.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EnvironmentStatusKind } from "./EnvironmentStatusKind"; + +/** + * Current status for the requested environment. + */ +export type EnvironmentStatusResponse = { + /** + * Current status observed without starting or recovering the environment. + */ + status: EnvironmentStatusKind; + /** + * Human-readable detail for `disconnected` and `unknown`; omitted for other statuses. + */ + error?: string; +}; diff --git a/src/runtimes/openai/generated/v2/ErrorNotification.ts b/src/runtimes/openai/generated/v2/ErrorNotification.ts new file mode 100644 index 0000000..220d462 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ErrorNotification.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TurnError } from "./TurnError"; + +export type ErrorNotification = { + error: TurnError; + willRetry: boolean; + threadId: string; + turnId: string; +}; diff --git a/src/runtimes/openai/generated/v2/ExecPolicyAmendment.ts b/src/runtimes/openai/generated/v2/ExecPolicyAmendment.ts new file mode 100644 index 0000000..e893dd4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExecPolicyAmendment.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExecPolicyAmendment = Array; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeature.ts b/src/runtimes/openai/generated/v2/ExperimentalFeature.ts new file mode 100644 index 0000000..6c4d048 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExperimentalFeature.ts @@ -0,0 +1,38 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExperimentalFeatureStage } from "./ExperimentalFeatureStage"; + +export type ExperimentalFeature = { + /** + * Stable key used in config.toml and CLI flag toggles. + */ + name: string; + /** + * Lifecycle stage of this feature flag. + */ + stage: ExperimentalFeatureStage; + /** + * User-facing display name shown in the experimental features UI. + * Null when this feature is not in beta. + */ + displayName: string | null; + /** + * Short summary describing what the feature does. + * Null when this feature is not in beta. + */ + description: string | null; + /** + * Announcement copy shown to users when the feature is introduced. + * Null when this feature is not in beta. + */ + announcement: string | null; + /** + * Whether this feature is currently enabled in the loaded config. + */ + enabled: boolean; + /** + * Whether this feature is enabled by default. + */ + defaultEnabled: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetParams.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetParams.ts new file mode 100644 index 0000000..de388e3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExperimentalFeatureEnablementSetParams = { + /** + * Process-wide runtime feature enablement keyed by canonical feature name. + * + * Only named features are updated. Omitted features are left unchanged. + * Send an empty map for a no-op. + */ + enablement: { [key in string]?: boolean }; +}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetResponse.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetResponse.ts new file mode 100644 index 0000000..e3349d1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExperimentalFeatureEnablementSetResponse = { + /** + * Feature enablement entries updated by this request. + */ + enablement: { [key in string]?: boolean }; +}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureListParams.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureListParams.ts new file mode 100644 index 0000000..3d55d59 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExperimentalFeatureListParams.ts @@ -0,0 +1,20 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExperimentalFeatureListParams = { + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size; defaults to a reasonable server-side value. + */ + limit?: number | null; + /** + * Optional loaded thread id. Pass this when showing feature state for an + * existing thread so enablement is computed from that thread's refreshed + * config, including project-local config for the thread's cwd. + */ + threadId?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureListResponse.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureListResponse.ts new file mode 100644 index 0000000..37c64b6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExperimentalFeatureListResponse.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExperimentalFeature } from "./ExperimentalFeature"; + +export type ExperimentalFeatureListResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * If None, there are no more items to return. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureStage.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureStage.ts new file mode 100644 index 0000000..b72828d --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExperimentalFeatureStage.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExperimentalFeatureStage = + | "beta" + | "underDevelopment" + | "stable" + | "deprecated" + | "removed"; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectParams.ts new file mode 100644 index 0000000..eea4470 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectParams.ts @@ -0,0 +1,31 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExternalAgentConfigDetectParams = { + /** + * If true, include detection under the user's home directory. + */ + includeHome?: boolean; + /** + * Zero or more working directories to include for repo-scoped detection. + */ + cwds?: Array | null; + /** + * Maximum age in days for detected sessions. Missing values use the default limit. + */ + maxSessionAgeDays?: number | null; + /** + * Maximum number of sessions to detect. Missing values use the default limit. + */ + maxSessions?: number | null; + /** + * Deprecated field retained for compatibility. This field is ignored; use `migrationSource` + * to select the migration source. + */ + source?: string | null; + /** + * Optional migration-source selector. Missing or unrecognized values use the default source. + */ + migrationSource?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectResponse.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectResponse.ts new file mode 100644 index 0000000..2f7e40a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; +import type { ExternalAgentDetectedConnectorCandidate } from "./ExternalAgentDetectedConnectorCandidate"; + +export type ExternalAgentConfigDetectResponse = { + items: Array; + connectors: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportCompletedNotification.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportCompletedNotification.ts new file mode 100644 index 0000000..df10dfe --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportCompletedNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigImportTypeResult } from "./ExternalAgentConfigImportTypeResult"; + +export type ExternalAgentConfigImportCompletedNotification = { + importId: string; + itemTypeResults: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.ts new file mode 100644 index 0000000..bad6aa3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigImportHistory } from "./ExternalAgentConfigImportHistory"; +import type { ExternalAgentImportedConnectorCandidate } from "./ExternalAgentImportedConnectorCandidate"; + +export type ExternalAgentConfigImportHistoriesReadResponse = { + data: Array; + connectors: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistory.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistory.ts new file mode 100644 index 0000000..31c216f --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistory.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigImportItemTypeFailure } from "./ExternalAgentConfigImportItemTypeFailure"; +import type { ExternalAgentConfigImportItemTypeSuccess } from "./ExternalAgentConfigImportItemTypeSuccess"; + +export type ExternalAgentConfigImportHistory = { + importId: string; + providerId: string | null; + completedAtMs: bigint; + successes: Array; + failures: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordParams.ts new file mode 100644 index 0000000..21418af --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordParams.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigImportHistoryRecordTypeResultParams } from "./ExternalAgentConfigImportHistoryRecordTypeResultParams"; + +export type ExternalAgentConfigImportHistoryRecordParams = { + /** + * Opaque provider identifier for the externally completed import. + */ + providerId: string; + /** + * Completed results grouped by imported item type. + */ + itemTypeResults: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordResponse.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordResponse.ts new file mode 100644 index 0000000..ae38e7c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExternalAgentConfigImportHistoryRecordResponse = { importId: string }; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordSuccessParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordSuccessParams.ts new file mode 100644 index 0000000..5d644a3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordSuccessParams.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; + +export type ExternalAgentConfigImportHistoryRecordSuccessParams = { + itemType: ExternalAgentConfigMigrationItemType; + cwd: string | null; + source: string | null; + target: string | null; + /** + * Original title for an imported session, when available. + */ + title?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordTypeResultParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordTypeResultParams.ts new file mode 100644 index 0000000..21f46d9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordTypeResultParams.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigImportHistoryRecordSuccessParams } from "./ExternalAgentConfigImportHistoryRecordSuccessParams"; +import type { ExternalAgentConfigImportItemTypeFailure } from "./ExternalAgentConfigImportItemTypeFailure"; +import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; + +export type ExternalAgentConfigImportHistoryRecordTypeResultParams = { + itemType: ExternalAgentConfigMigrationItemType; + successes: Array; + failures: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeFailure.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeFailure.ts new file mode 100644 index 0000000..8677fe9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeFailure.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; + +export type ExternalAgentConfigImportItemTypeFailure = { + itemType: ExternalAgentConfigMigrationItemType; + errorType: string | null; + subErrorType: string | null; + failureStage: string; + message: string; + cwd: string | null; + source: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeSuccess.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeSuccess.ts new file mode 100644 index 0000000..6e26659 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeSuccess.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; + +export type ExternalAgentConfigImportItemTypeSuccess = { + itemType: ExternalAgentConfigMigrationItemType; + cwd: string | null; + source: string | null; + target: string | null; + /** + * Original title for an imported session; null for other item types. + */ + title: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportParams.ts new file mode 100644 index 0000000..8f14cfe --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportParams.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; + +export type ExternalAgentConfigImportParams = { + migrationItems: Array; + /** + * Optional identifier for the product that initiated the import. + */ + source?: string | null; + /** + * Opaque provider identifier supplied by the caller for analytics attribution and import + * history display. This does not select the migration source. + */ + providerId?: string | null; + /** + * Migration-source selector used to produce the migration items. Pass the same value to + * detection and import; missing or unrecognized values use the default source. + */ + migrationSource?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportProgressNotification.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportProgressNotification.ts new file mode 100644 index 0000000..6ee91b0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportProgressNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigImportTypeResult } from "./ExternalAgentConfigImportTypeResult"; + +export type ExternalAgentConfigImportProgressNotification = { + importId: string; + itemTypeResults: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportResponse.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportResponse.ts new file mode 100644 index 0000000..de0b71a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExternalAgentConfigImportResponse = { importId: string }; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportTypeResult.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportTypeResult.ts new file mode 100644 index 0000000..c8f701a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportTypeResult.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigImportItemTypeFailure } from "./ExternalAgentConfigImportItemTypeFailure"; +import type { ExternalAgentConfigImportItemTypeSuccess } from "./ExternalAgentConfigImportItemTypeSuccess"; +import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; + +export type ExternalAgentConfigImportTypeResult = { + itemType: ExternalAgentConfigMigrationItemType; + successes: Array; + failures: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItem.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItem.ts new file mode 100644 index 0000000..3a58d35 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItem.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; +import type { MigrationDetails } from "./MigrationDetails"; + +export type ExternalAgentConfigMigrationItem = { + itemType: ExternalAgentConfigMigrationItemType; + description: string; + /** + * Null or empty means home-scoped migration; non-empty means repo-scoped migration. + */ + cwd: string | null; + details: MigrationDetails | null; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItemType.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItemType.ts new file mode 100644 index 0000000..5116e89 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItemType.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "MEMORY" + | "SESSIONS"; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorCandidate.ts b/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorCandidate.ts new file mode 100644 index 0000000..fb12fc4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorCandidate.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentDetectedConnectorSource } from "./ExternalAgentDetectedConnectorSource"; + +export type ExternalAgentDetectedConnectorCandidate = { + name: string; + sessionCount: number; + source: ExternalAgentDetectedConnectorSource; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorSource.ts b/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorSource.ts new file mode 100644 index 0000000..c4c608c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorSource.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExternalAgentDetectedConnectorSource = "remoteMcpServersConfig" | "sessionToolUse"; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorCandidate.ts b/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorCandidate.ts new file mode 100644 index 0000000..d4f7321 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorCandidate.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentImportedConnectorSource } from "./ExternalAgentImportedConnectorSource"; + +export type ExternalAgentImportedConnectorCandidate = { + name: string; + sessionCount: number; + source: ExternalAgentImportedConnectorSource; +}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorSource.ts b/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorSource.ts new file mode 100644 index 0000000..5398eb4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorSource.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExternalAgentImportedConnectorSource = "remoteMcpServersConfig"; diff --git a/src/runtimes/openai/generated/v2/FeedbackRequirements.ts b/src/runtimes/openai/generated/v2/FeedbackRequirements.ts new file mode 100644 index 0000000..be44492 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FeedbackRequirements.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FeedbackRequirements = { enabled: boolean | null }; diff --git a/src/runtimes/openai/generated/v2/FeedbackUploadParams.ts b/src/runtimes/openai/generated/v2/FeedbackUploadParams.ts new file mode 100644 index 0000000..21702cb --- /dev/null +++ b/src/runtimes/openai/generated/v2/FeedbackUploadParams.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FeedbackUploadParams = { + classification: string; + reason?: string | null; + threadId?: string | null; + includeLogs?: boolean; + extraLogFiles?: Array | null; + tags?: { [key in string]?: string } | null; +}; diff --git a/src/runtimes/openai/generated/v2/FeedbackUploadResponse.ts b/src/runtimes/openai/generated/v2/FeedbackUploadResponse.ts new file mode 100644 index 0000000..6b2db64 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FeedbackUploadResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FeedbackUploadResponse = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/FileChangeApprovalDecision.ts b/src/runtimes/openai/generated/v2/FileChangeApprovalDecision.ts new file mode 100644 index 0000000..b74ba00 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileChangeApprovalDecision.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FileChangeApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel"; diff --git a/src/runtimes/openai/generated/v2/FileChangeOutputDeltaNotification.ts b/src/runtimes/openai/generated/v2/FileChangeOutputDeltaNotification.ts new file mode 100644 index 0000000..abd645c --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileChangeOutputDeltaNotification.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Deprecated legacy notification for `apply_patch` textual output. + * + * The server no longer emits this notification. + */ +export type FileChangeOutputDeltaNotification = { + threadId: string; + turnId: string; + itemId: string; + delta: string; +}; diff --git a/src/runtimes/openai/generated/v2/FileChangePatchUpdatedNotification.ts b/src/runtimes/openai/generated/v2/FileChangePatchUpdatedNotification.ts new file mode 100644 index 0000000..ccc7811 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileChangePatchUpdatedNotification.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FileUpdateChange } from "./FileUpdateChange"; + +export type FileChangePatchUpdatedNotification = { + threadId: string; + turnId: string; + itemId: string; + changes: Array; +}; diff --git a/src/runtimes/openai/generated/v2/FileChangeRequestApprovalParams.ts b/src/runtimes/openai/generated/v2/FileChangeRequestApprovalParams.ts new file mode 100644 index 0000000..e01a715 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileChangeRequestApprovalParams.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FileChangeRequestApprovalParams = { + threadId: string; + turnId: string; + itemId: string; + /** + * Unix timestamp (in milliseconds) when this approval request started. + */ + startedAtMs: number; + /** + * Optional explanatory reason (e.g. request for extra write access). + */ + reason?: string | null; + /** + * [UNSTABLE] When set, the agent is asking the user to allow writes under this root + * for the remainder of the session (unclear if this is honored today). + */ + grantRoot?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/FileChangeRequestApprovalResponse.ts b/src/runtimes/openai/generated/v2/FileChangeRequestApprovalResponse.ts new file mode 100644 index 0000000..621b6c4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileChangeRequestApprovalResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision"; + +export type FileChangeRequestApprovalResponse = { decision: FileChangeApprovalDecision }; diff --git a/src/runtimes/openai/generated/v2/FileSystemAccessMode.ts b/src/runtimes/openai/generated/v2/FileSystemAccessMode.ts new file mode 100644 index 0000000..2dac727 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileSystemAccessMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FileSystemAccessMode = "read" | "write" | "deny"; diff --git a/src/runtimes/openai/generated/v2/FileSystemPath.ts b/src/runtimes/openai/generated/v2/FileSystemPath.ts new file mode 100644 index 0000000..b67e995 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileSystemPath.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LegacyAppPathString } from "../LegacyAppPathString"; +import type { FileSystemSpecialPath } from "./FileSystemSpecialPath"; + +export type FileSystemPath = + | { type: "path"; path: LegacyAppPathString } + | { type: "glob_pattern"; pattern: string } + | { type: "special"; value: FileSystemSpecialPath }; diff --git a/src/runtimes/openai/generated/v2/FileSystemSandboxEntry.ts b/src/runtimes/openai/generated/v2/FileSystemSandboxEntry.ts new file mode 100644 index 0000000..e1a6224 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileSystemSandboxEntry.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FileSystemAccessMode } from "./FileSystemAccessMode"; +import type { FileSystemPath } from "./FileSystemPath"; + +export type FileSystemSandboxEntry = { path: FileSystemPath; access: FileSystemAccessMode }; diff --git a/src/runtimes/openai/generated/v2/FileSystemSpecialPath.ts b/src/runtimes/openai/generated/v2/FileSystemSpecialPath.ts new file mode 100644 index 0000000..67a132a --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileSystemSpecialPath.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LegacyAppPathString } from "../LegacyAppPathString"; + +export type FileSystemSpecialPath = + | { kind: "root" } + | { kind: "minimal" } + | { kind: "project_roots"; subpath: LegacyAppPathString | null } + | { kind: "tmpdir" } + | { kind: "slash_tmp" } + | { kind: "unknown"; path: string; subpath: LegacyAppPathString | null }; diff --git a/src/runtimes/openai/generated/v2/FileUpdateChange.ts b/src/runtimes/openai/generated/v2/FileUpdateChange.ts new file mode 100644 index 0000000..be1c4ce --- /dev/null +++ b/src/runtimes/openai/generated/v2/FileUpdateChange.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PatchChangeKind } from "./PatchChangeKind"; + +export type FileUpdateChange = { path: string; kind: PatchChangeKind; diff: string }; diff --git a/src/runtimes/openai/generated/v2/ForcedChatgptWorkspaceIds.ts b/src/runtimes/openai/generated/v2/ForcedChatgptWorkspaceIds.ts new file mode 100644 index 0000000..d0582c8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ForcedChatgptWorkspaceIds.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Backward-compatible API shape for ChatGPT workspace login restrictions. + */ +export type ForcedChatgptWorkspaceIds = string | Array; diff --git a/src/runtimes/openai/generated/v2/FsChangedNotification.ts b/src/runtimes/openai/generated/v2/FsChangedNotification.ts new file mode 100644 index 0000000..fe64bdc --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsChangedNotification.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Filesystem watch notification emitted for `fs/watch` subscribers. + */ +export type FsChangedNotification = { + /** + * Watch identifier previously provided to `fs/watch`. + */ + watchId: string; + /** + * File or directory paths associated with this event. + */ + changedPaths: Array; +}; diff --git a/src/runtimes/openai/generated/v2/FsCopyParams.ts b/src/runtimes/openai/generated/v2/FsCopyParams.ts new file mode 100644 index 0000000..2eec6d9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsCopyParams.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Copy a file or directory tree on the host filesystem. + */ +export type FsCopyParams = { + /** + * Absolute source path. + */ + sourcePath: AbsolutePathBuf; + /** + * Absolute destination path. + */ + destinationPath: AbsolutePathBuf; + /** + * Required for directory copies; ignored for file copies. + */ + recursive?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/FsCopyResponse.ts b/src/runtimes/openai/generated/v2/FsCopyResponse.ts new file mode 100644 index 0000000..3e3061a --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsCopyResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/copy`. + */ +export type FsCopyResponse = Record; diff --git a/src/runtimes/openai/generated/v2/FsCreateDirectoryParams.ts b/src/runtimes/openai/generated/v2/FsCreateDirectoryParams.ts new file mode 100644 index 0000000..334cfc7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsCreateDirectoryParams.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Create a directory on the host filesystem. + */ +export type FsCreateDirectoryParams = { + /** + * Absolute directory path to create. + */ + path: AbsolutePathBuf; + /** + * Whether parent directories should also be created. Defaults to `true`. + */ + recursive?: boolean | null; +}; diff --git a/src/runtimes/openai/generated/v2/FsCreateDirectoryResponse.ts b/src/runtimes/openai/generated/v2/FsCreateDirectoryResponse.ts new file mode 100644 index 0000000..5d251b7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsCreateDirectoryResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/createDirectory`. + */ +export type FsCreateDirectoryResponse = Record; diff --git a/src/runtimes/openai/generated/v2/FsGetMetadataParams.ts b/src/runtimes/openai/generated/v2/FsGetMetadataParams.ts new file mode 100644 index 0000000..f03e66f --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsGetMetadataParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Request metadata for an absolute path. + */ +export type FsGetMetadataParams = { + /** + * Absolute path to inspect. + */ + path: AbsolutePathBuf; +}; diff --git a/src/runtimes/openai/generated/v2/FsGetMetadataResponse.ts b/src/runtimes/openai/generated/v2/FsGetMetadataResponse.ts new file mode 100644 index 0000000..14352b9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsGetMetadataResponse.ts @@ -0,0 +1,29 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Metadata returned by `fs/getMetadata`. + */ +export type FsGetMetadataResponse = { + /** + * Whether the path resolves to a directory. + */ + isDirectory: boolean; + /** + * Whether the path resolves to a regular file. + */ + isFile: boolean; + /** + * Whether the path itself is a symbolic link. + */ + isSymlink: boolean; + /** + * File creation time in Unix milliseconds when available, otherwise `0`. + */ + createdAtMs: number; + /** + * File modification time in Unix milliseconds when available, otherwise `0`. + */ + modifiedAtMs: number; +}; diff --git a/src/runtimes/openai/generated/v2/FsReadDirectoryEntry.ts b/src/runtimes/openai/generated/v2/FsReadDirectoryEntry.ts new file mode 100644 index 0000000..0d44a75 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsReadDirectoryEntry.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * A directory entry returned by `fs/readDirectory`. + */ +export type FsReadDirectoryEntry = { + /** + * Direct child entry name only, not an absolute or relative path. + */ + fileName: string; + /** + * Whether this entry resolves to a directory. + */ + isDirectory: boolean; + /** + * Whether this entry resolves to a regular file. + */ + isFile: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/FsReadDirectoryParams.ts b/src/runtimes/openai/generated/v2/FsReadDirectoryParams.ts new file mode 100644 index 0000000..08310e6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsReadDirectoryParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * List direct child names for a directory. + */ +export type FsReadDirectoryParams = { + /** + * Absolute directory path to read. + */ + path: AbsolutePathBuf; +}; diff --git a/src/runtimes/openai/generated/v2/FsReadDirectoryResponse.ts b/src/runtimes/openai/generated/v2/FsReadDirectoryResponse.ts new file mode 100644 index 0000000..49e9680 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsReadDirectoryResponse.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FsReadDirectoryEntry } from "./FsReadDirectoryEntry"; + +/** + * Directory entries returned by `fs/readDirectory`. + */ +export type FsReadDirectoryResponse = { + /** + * Direct child entries in the requested directory. + */ + entries: Array; +}; diff --git a/src/runtimes/openai/generated/v2/FsReadFileParams.ts b/src/runtimes/openai/generated/v2/FsReadFileParams.ts new file mode 100644 index 0000000..cb71e2b --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsReadFileParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Read a file from the host filesystem. + */ +export type FsReadFileParams = { + /** + * Absolute path to read. + */ + path: AbsolutePathBuf; +}; diff --git a/src/runtimes/openai/generated/v2/FsReadFileResponse.ts b/src/runtimes/openai/generated/v2/FsReadFileResponse.ts new file mode 100644 index 0000000..15906af --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsReadFileResponse.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Base64-encoded file contents returned by `fs/readFile`. + */ +export type FsReadFileResponse = { + /** + * File contents encoded as base64. + */ + dataBase64: string; +}; diff --git a/src/runtimes/openai/generated/v2/FsRemoveParams.ts b/src/runtimes/openai/generated/v2/FsRemoveParams.ts new file mode 100644 index 0000000..dc3948b --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsRemoveParams.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Remove a file or directory tree from the host filesystem. + */ +export type FsRemoveParams = { + /** + * Absolute path to remove. + */ + path: AbsolutePathBuf; + /** + * Whether directory removal should recurse. Defaults to `true`. + */ + recursive?: boolean | null; + /** + * Whether missing paths should be ignored. Defaults to `true`. + */ + force?: boolean | null; +}; diff --git a/src/runtimes/openai/generated/v2/FsRemoveResponse.ts b/src/runtimes/openai/generated/v2/FsRemoveResponse.ts new file mode 100644 index 0000000..981c28f --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsRemoveResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/remove`. + */ +export type FsRemoveResponse = Record; diff --git a/src/runtimes/openai/generated/v2/FsUnwatchParams.ts b/src/runtimes/openai/generated/v2/FsUnwatchParams.ts new file mode 100644 index 0000000..d3f6198 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsUnwatchParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Stop filesystem watch notifications for a prior `fs/watch`. + */ +export type FsUnwatchParams = { + /** + * Watch identifier previously provided to `fs/watch`. + */ + watchId: string; +}; diff --git a/src/runtimes/openai/generated/v2/FsUnwatchResponse.ts b/src/runtimes/openai/generated/v2/FsUnwatchResponse.ts new file mode 100644 index 0000000..02507d2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsUnwatchResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/unwatch`. + */ +export type FsUnwatchResponse = Record; diff --git a/src/runtimes/openai/generated/v2/FsWatchParams.ts b/src/runtimes/openai/generated/v2/FsWatchParams.ts new file mode 100644 index 0000000..342815e --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsWatchParams.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Start filesystem watch notifications for an absolute path. + */ +export type FsWatchParams = { + /** + * Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`. + */ + watchId: string; + /** + * Absolute file or directory path to watch. + */ + path: AbsolutePathBuf; +}; diff --git a/src/runtimes/openai/generated/v2/FsWatchResponse.ts b/src/runtimes/openai/generated/v2/FsWatchResponse.ts new file mode 100644 index 0000000..7b3459f --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsWatchResponse.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Successful response for `fs/watch`. + */ +export type FsWatchResponse = { + /** + * Canonicalized path associated with the watch. + */ + path: AbsolutePathBuf; +}; diff --git a/src/runtimes/openai/generated/v2/FsWriteFileParams.ts b/src/runtimes/openai/generated/v2/FsWriteFileParams.ts new file mode 100644 index 0000000..c10e70e --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsWriteFileParams.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Write a file on the host filesystem. + */ +export type FsWriteFileParams = { + /** + * Absolute path to write. + */ + path: AbsolutePathBuf; + /** + * File contents encoded as base64. + */ + dataBase64: string; +}; diff --git a/src/runtimes/openai/generated/v2/FsWriteFileResponse.ts b/src/runtimes/openai/generated/v2/FsWriteFileResponse.ts new file mode 100644 index 0000000..ad0ce28 --- /dev/null +++ b/src/runtimes/openai/generated/v2/FsWriteFileResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/writeFile`. + */ +export type FsWriteFileResponse = Record; diff --git a/src/runtimes/openai/generated/v2/GetAccountParams.ts b/src/runtimes/openai/generated/v2/GetAccountParams.ts new file mode 100644 index 0000000..7d8f1d6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GetAccountParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GetAccountParams = { + /** + * When `true`, requests a proactive token refresh before returning. + * + * In managed auth mode this triggers the normal refresh-token flow. In + * external auth mode this flag is ignored. Clients should refresh tokens + * themselves and call `account/login/start` with `chatgptAuthTokens`. + */ + refreshToken?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/GetAccountRateLimitsResponse.ts b/src/runtimes/openai/generated/v2/GetAccountRateLimitsResponse.ts new file mode 100644 index 0000000..136e934 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GetAccountRateLimitsResponse.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimitResetCreditsSummary } from "./RateLimitResetCreditsSummary"; +import type { RateLimitSnapshot } from "./RateLimitSnapshot"; + +export type GetAccountRateLimitsResponse = { + /** + * Backward-compatible single-bucket view; mirrors the historical payload. + */ + rateLimits: RateLimitSnapshot; + /** + * Multi-bucket view keyed by metered `limit_id` (for example, `codex`). + */ + rateLimitsByLimitId: { [key in string]?: RateLimitSnapshot } | null; + rateLimitResetCredits: RateLimitResetCreditsSummary | null; +}; diff --git a/src/runtimes/openai/generated/v2/GetAccountResponse.ts b/src/runtimes/openai/generated/v2/GetAccountResponse.ts new file mode 100644 index 0000000..75f09dd --- /dev/null +++ b/src/runtimes/openai/generated/v2/GetAccountResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Account } from "./Account"; + +export type GetAccountResponse = { account: Account | null; requiresOpenaiAuth: boolean }; diff --git a/src/runtimes/openai/generated/v2/GetAccountTokenUsageParams.ts b/src/runtimes/openai/generated/v2/GetAccountTokenUsageParams.ts new file mode 100644 index 0000000..59709e9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GetAccountTokenUsageParams.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GetAccountTokenUsageParams = { + /** + * When present, read estimated usage for this thread instead of account-wide token activity. + */ + threadId?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/GetAccountTokenUsageResponse.ts b/src/runtimes/openai/generated/v2/GetAccountTokenUsageResponse.ts new file mode 100644 index 0000000..88f8725 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GetAccountTokenUsageResponse.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket"; +import type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary"; +import type { ThreadUsage } from "./ThreadUsage"; + +export type GetAccountTokenUsageResponse = { + summary: AccountTokenUsageSummary; + dailyUsageBuckets: Array | null; + /** + * Estimated usage when a thread was requested and its billing route is available. + */ + threadUsage?: ThreadUsage | null; +}; diff --git a/src/runtimes/openai/generated/v2/GetWorkspaceMessagesResponse.ts b/src/runtimes/openai/generated/v2/GetWorkspaceMessagesResponse.ts new file mode 100644 index 0000000..8f1c537 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GetWorkspaceMessagesResponse.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WorkspaceMessage } from "./WorkspaceMessage"; + +export type GetWorkspaceMessagesResponse = { + /** + * Whether the workspace-message backend route is available for this client. + */ + featureEnabled: boolean; + /** + * Active workspace messages returned by the backend. + */ + messages: Array; +}; diff --git a/src/runtimes/openai/generated/v2/GitInfo.ts b/src/runtimes/openai/generated/v2/GitInfo.ts new file mode 100644 index 0000000..ea62ce2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GitInfo.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GitInfo = { sha: string | null; branch: string | null; originUrl: string | null }; diff --git a/src/runtimes/openai/generated/v2/GrantedPermissionProfile.ts b/src/runtimes/openai/generated/v2/GrantedPermissionProfile.ts new file mode 100644 index 0000000..47b41a1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GrantedPermissionProfile.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; +import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; + +export type GrantedPermissionProfile = { + network?: AdditionalNetworkPermissions; + fileSystem?: AdditionalFileSystemPermissions; +}; diff --git a/src/runtimes/openai/generated/v2/GuardianApprovalReview.ts b/src/runtimes/openai/generated/v2/GuardianApprovalReview.ts new file mode 100644 index 0000000..1b466a5 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GuardianApprovalReview.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { GuardianApprovalReviewStatus } from "./GuardianApprovalReviewStatus"; +import type { GuardianRiskLevel } from "./GuardianRiskLevel"; +import type { GuardianUserAuthorization } from "./GuardianUserAuthorization"; + +/** + * [UNSTABLE] Temporary approval auto-review payload used by + * `item/autoApprovalReview/*` notifications. This shape is expected to change + * soon. + */ +export type GuardianApprovalReview = { + status: GuardianApprovalReviewStatus; + riskLevel: GuardianRiskLevel | null; + userAuthorization: GuardianUserAuthorization | null; + rationale: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/GuardianApprovalReviewAction.ts b/src/runtimes/openai/generated/v2/GuardianApprovalReviewAction.ts new file mode 100644 index 0000000..e7e40d3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GuardianApprovalReviewAction.ts @@ -0,0 +1,42 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { LegacyAppPathString } from "../LegacyAppPathString"; +import type { GuardianCommandSource } from "./GuardianCommandSource"; +import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; +import type { RequestPermissionProfile } from "./RequestPermissionProfile"; + +export type GuardianApprovalReviewAction = + | { type: "command"; source: GuardianCommandSource; command: string; cwd: AbsolutePathBuf } + | { + type: "execve"; + source: GuardianCommandSource; + program: string; + argv: Array; + cwd: AbsolutePathBuf; + } + | { + type: "writeStdin"; + approvalId: string; + processId: string; + stdin: string; + cwd: LegacyAppPathString; + } + | { type: "applyPatch"; cwd: AbsolutePathBuf; files: Array } + | { + type: "networkAccess"; + target: string; + host: string; + protocol: NetworkApprovalProtocol; + port: number; + } + | { + type: "mcpToolCall"; + server: string; + toolName: string; + connectorId: string | null; + connectorName: string | null; + toolTitle: string | null; + } + | { type: "requestPermissions"; reason: string | null; permissions: RequestPermissionProfile }; diff --git a/src/runtimes/openai/generated/v2/GuardianApprovalReviewStatus.ts b/src/runtimes/openai/generated/v2/GuardianApprovalReviewStatus.ts new file mode 100644 index 0000000..7e84c40 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GuardianApprovalReviewStatus.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * [UNSTABLE] Lifecycle state for an approval auto-review. + */ +export type GuardianApprovalReviewStatus = + | "inProgress" + | "approved" + | "denied" + | "timedOut" + | "aborted"; diff --git a/src/runtimes/openai/generated/v2/GuardianCommandSource.ts b/src/runtimes/openai/generated/v2/GuardianCommandSource.ts new file mode 100644 index 0000000..b48e9b0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GuardianCommandSource.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GuardianCommandSource = "shell" | "unifiedExec"; diff --git a/src/runtimes/openai/generated/v2/GuardianRiskLevel.ts b/src/runtimes/openai/generated/v2/GuardianRiskLevel.ts new file mode 100644 index 0000000..7734016 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GuardianRiskLevel.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * [UNSTABLE] Risk level assigned by approval auto-review. + */ +export type GuardianRiskLevel = "low" | "medium" | "high" | "critical"; diff --git a/src/runtimes/openai/generated/v2/GuardianUserAuthorization.ts b/src/runtimes/openai/generated/v2/GuardianUserAuthorization.ts new file mode 100644 index 0000000..936611f --- /dev/null +++ b/src/runtimes/openai/generated/v2/GuardianUserAuthorization.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * [UNSTABLE] Authorization level assigned by approval auto-review. + */ +export type GuardianUserAuthorization = "unknown" | "low" | "medium" | "high"; diff --git a/src/runtimes/openai/generated/v2/GuardianWarningNotification.ts b/src/runtimes/openai/generated/v2/GuardianWarningNotification.ts new file mode 100644 index 0000000..763a0f6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/GuardianWarningNotification.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GuardianWarningNotification = { + /** + * Thread target for the guardian warning. + */ + threadId: string; + /** + * Concise guardian warning message for the user. + */ + message: string; +}; diff --git a/src/runtimes/openai/generated/v2/HookCompletedNotification.ts b/src/runtimes/openai/generated/v2/HookCompletedNotification.ts new file mode 100644 index 0000000..49bffae --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookCompletedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HookRunSummary } from "./HookRunSummary"; + +export type HookCompletedNotification = { + threadId: string; + turnId: string | null; + run: HookRunSummary; +}; diff --git a/src/runtimes/openai/generated/v2/HookErrorInfo.ts b/src/runtimes/openai/generated/v2/HookErrorInfo.ts new file mode 100644 index 0000000..9d66073 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookErrorInfo.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookErrorInfo = { path: string; message: string }; diff --git a/src/runtimes/openai/generated/v2/HookEventName.ts b/src/runtimes/openai/generated/v2/HookEventName.ts new file mode 100644 index 0000000..fe92ff5 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookEventName.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookEventName = + | "preToolUse" + | "permissionRequest" + | "postToolUse" + | "preCompact" + | "postCompact" + | "sessionStart" + | "sessionEnd" + | "userPromptSubmit" + | "subagentStart" + | "subagentStop" + | "stop" + | "interrupt"; diff --git a/src/runtimes/openai/generated/v2/HookExecutionMode.ts b/src/runtimes/openai/generated/v2/HookExecutionMode.ts new file mode 100644 index 0000000..61f9856 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookExecutionMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookExecutionMode = "sync" | "async"; diff --git a/src/runtimes/openai/generated/v2/HookHandlerType.ts b/src/runtimes/openai/generated/v2/HookHandlerType.ts new file mode 100644 index 0000000..7dd671e --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookHandlerType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookHandlerType = "command" | "mcpTool" | "prompt" | "agent"; diff --git a/src/runtimes/openai/generated/v2/HookMetadata.ts b/src/runtimes/openai/generated/v2/HookMetadata.ts new file mode 100644 index 0000000..b7f40b5 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookMetadata.ts @@ -0,0 +1,33 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { HookEventName } from "./HookEventName"; +import type { HookSource } from "./HookSource"; +import type { HookTrustStatus } from "./HookTrustStatus"; + +export type HookMetadata = { + key: string; + eventName: HookEventName; + matcher: string | null; + timeoutSec: bigint; + statusMessage: string | null; + /** + * Configured `additionalContext` spill threshold. + * `null` uses 2,500 tokens; `0` disables spilling. + */ + additionalContextLimit: number | null; + sourcePath: AbsolutePathBuf; + source: HookSource; + pluginId: string | null; + displayOrder: bigint; + enabled: boolean; + isManaged: boolean; + currentHash: string; + trustStatus: HookTrustStatus; +} & ( + | { handlerType: "command"; command: string; async: boolean } + | { handlerType: "mcpTool"; server: string; tool: string } + | { handlerType: "prompt" } + | { handlerType: "agent" } +); diff --git a/src/runtimes/openai/generated/v2/HookMigration.ts b/src/runtimes/openai/generated/v2/HookMigration.ts new file mode 100644 index 0000000..4f4b3f3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookMigration.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/HookOutputEntry.ts b/src/runtimes/openai/generated/v2/HookOutputEntry.ts new file mode 100644 index 0000000..9f02b87 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookOutputEntry.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HookOutputEntryKind } from "./HookOutputEntryKind"; + +export type HookOutputEntry = { kind: HookOutputEntryKind; text: string }; diff --git a/src/runtimes/openai/generated/v2/HookOutputEntryKind.ts b/src/runtimes/openai/generated/v2/HookOutputEntryKind.ts new file mode 100644 index 0000000..090dfe3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookOutputEntryKind.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookOutputEntryKind = "warning" | "stop" | "feedback" | "context" | "error"; diff --git a/src/runtimes/openai/generated/v2/HookPromptFragment.ts b/src/runtimes/openai/generated/v2/HookPromptFragment.ts new file mode 100644 index 0000000..74f2455 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookPromptFragment.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookPromptFragment = { text: string; hookRunId: string }; diff --git a/src/runtimes/openai/generated/v2/HookRunStatus.ts b/src/runtimes/openai/generated/v2/HookRunStatus.ts new file mode 100644 index 0000000..ffca7e0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookRunStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookRunStatus = "running" | "completed" | "failed" | "blocked" | "stopped"; diff --git a/src/runtimes/openai/generated/v2/HookRunSummary.ts b/src/runtimes/openai/generated/v2/HookRunSummary.ts new file mode 100644 index 0000000..2c2fc5d --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookRunSummary.ts @@ -0,0 +1,28 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { HookEventName } from "./HookEventName"; +import type { HookExecutionMode } from "./HookExecutionMode"; +import type { HookHandlerType } from "./HookHandlerType"; +import type { HookOutputEntry } from "./HookOutputEntry"; +import type { HookRunStatus } from "./HookRunStatus"; +import type { HookScope } from "./HookScope"; +import type { HookSource } from "./HookSource"; + +export type HookRunSummary = { + id: string; + eventName: HookEventName; + handlerType: HookHandlerType; + executionMode: HookExecutionMode; + scope: HookScope; + sourcePath: AbsolutePathBuf; + source: HookSource; + displayOrder: bigint; + status: HookRunStatus; + statusMessage: string | null; + startedAt: bigint; + completedAt: bigint | null; + durationMs: bigint | null; + entries: Array; +}; diff --git a/src/runtimes/openai/generated/v2/HookScope.ts b/src/runtimes/openai/generated/v2/HookScope.ts new file mode 100644 index 0000000..ff6f8bf --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookScope.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookScope = "thread" | "turn"; diff --git a/src/runtimes/openai/generated/v2/HookSource.ts b/src/runtimes/openai/generated/v2/HookSource.ts new file mode 100644 index 0000000..e9e4cac --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookSource.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookSource = + | "system" + | "user" + | "project" + | "mdm" + | "sessionFlags" + | "plugin" + | "cloudRequirements" + | "cloudManagedConfig" + | "legacyManagedConfigFile" + | "legacyManagedConfigMdm" + | "unknown"; diff --git a/src/runtimes/openai/generated/v2/HookStartedNotification.ts b/src/runtimes/openai/generated/v2/HookStartedNotification.ts new file mode 100644 index 0000000..880bb40 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookStartedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HookRunSummary } from "./HookRunSummary"; + +export type HookStartedNotification = { + threadId: string; + turnId: string | null; + run: HookRunSummary; +}; diff --git a/src/runtimes/openai/generated/v2/HookTrustStatus.ts b/src/runtimes/openai/generated/v2/HookTrustStatus.ts new file mode 100644 index 0000000..692fdc4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HookTrustStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HookTrustStatus = "managed" | "untrusted" | "trusted" | "modified"; diff --git a/src/runtimes/openai/generated/v2/HooksListEntry.ts b/src/runtimes/openai/generated/v2/HooksListEntry.ts new file mode 100644 index 0000000..354ead3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/HooksListEntry.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HookErrorInfo } from "./HookErrorInfo"; +import type { HookMetadata } from "./HookMetadata"; + +export type HooksListEntry = { + cwd: string; + hooks: Array; + warnings: Array; + errors: Array; +}; diff --git a/src/runtimes/openai/generated/v2/HooksListParams.ts b/src/runtimes/openai/generated/v2/HooksListParams.ts new file mode 100644 index 0000000..3f85dcb --- /dev/null +++ b/src/runtimes/openai/generated/v2/HooksListParams.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HooksListParams = { + /** + * When empty, defaults to the current session working directory. + */ + cwds?: Array; +}; diff --git a/src/runtimes/openai/generated/v2/HooksListResponse.ts b/src/runtimes/openai/generated/v2/HooksListResponse.ts new file mode 100644 index 0000000..44f439c --- /dev/null +++ b/src/runtimes/openai/generated/v2/HooksListResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HooksListEntry } from "./HooksListEntry"; + +export type HooksListResponse = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/InAppBrowserRequirements.ts b/src/runtimes/openai/generated/v2/InAppBrowserRequirements.ts new file mode 100644 index 0000000..45ab92d --- /dev/null +++ b/src/runtimes/openai/generated/v2/InAppBrowserRequirements.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type InAppBrowserRequirements = { allowExternalBrowserSettingsImport: boolean | null }; diff --git a/src/runtimes/openai/generated/v2/InstalledApp.ts b/src/runtimes/openai/generated/v2/InstalledApp.ts new file mode 100644 index 0000000..9ddd088 --- /dev/null +++ b/src/runtimes/openai/generated/v2/InstalledApp.ts @@ -0,0 +1,25 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Installed connector runtime state. + */ +export type InstalledApp = { + id: string; + /** + * Best-effort name carried by the runtime tool catalog. Canonical app metadata remains owned + * by `app/read`. + */ + runtimeName: string | null; + /** + * Effective enabled state after applying global, workspace, local, and managed configuration + * at read time. + */ + enabled: boolean; + /** + * Whether the connector is enabled and has a non-synthetic, model-visible tool allowed by + * effective MCP and app/tool policy in the committed runtime snapshot. + */ + callable: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ItemCompletedNotification.ts b/src/runtimes/openai/generated/v2/ItemCompletedNotification.ts new file mode 100644 index 0000000..18220a1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ItemCompletedNotification.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadItem } from "./ThreadItem"; + +export type ItemCompletedNotification = { + item: ThreadItem; + threadId: string; + turnId: string; + /** + * Unix timestamp (in milliseconds) when this item lifecycle completed. + */ + completedAtMs: number; +}; diff --git a/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewCompletedNotification.ts b/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewCompletedNotification.ts new file mode 100644 index 0000000..6d62f0a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewCompletedNotification.ts @@ -0,0 +1,46 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource"; +import type { GuardianApprovalReview } from "./GuardianApprovalReview"; +import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction"; + +/** + * [UNSTABLE] Temporary notification payload for approval auto-review. This + * shape is expected to change soon. + */ +export type ItemGuardianApprovalReviewCompletedNotification = { + threadId: string; + turnId: string; + /** + * Unix timestamp (in milliseconds) when this review started. + */ + startedAtMs: number; + /** + * Unix timestamp (in milliseconds) when this review completed. + */ + completedAtMs: number; + /** + * Stable identifier for this review. + */ + reviewId: string; + /** + * Identifier for the reviewed item or tool call when one exists. + * + * In most cases, one review maps to one target item. The exceptions are + * - execve reviews, where a single command may contain multiple execve + * calls to review (only possible when using the shell_zsh_fork feature) + * - stdin reviews, which refer to the existing parent command item and + * have a separate approval ID in the action payload + * - network policy reviews, where there is no target item + * + * A network call is triggered by a CommandExecution item, so having a + * target_item_id set to the CommandExecution item would be misleading + * because the review is about the network call, not the command execution. + * Therefore, target_item_id is set to None for network policy reviews. + */ + targetItemId: string | null; + decisionSource: AutoReviewDecisionSource; + review: GuardianApprovalReview; + action: GuardianApprovalReviewAction; +}; diff --git a/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewStartedNotification.ts b/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewStartedNotification.ts new file mode 100644 index 0000000..86e9a61 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewStartedNotification.ts @@ -0,0 +1,40 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { GuardianApprovalReview } from "./GuardianApprovalReview"; +import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction"; + +/** + * [UNSTABLE] Temporary notification payload for approval auto-review. This + * shape is expected to change soon. + */ +export type ItemGuardianApprovalReviewStartedNotification = { + threadId: string; + turnId: string; + /** + * Unix timestamp (in milliseconds) when this review started. + */ + startedAtMs: number; + /** + * Stable identifier for this review. + */ + reviewId: string; + /** + * Identifier for the reviewed item or tool call when one exists. + * + * In most cases, one review maps to one target item. The exceptions are + * - execve reviews, where a single command may contain multiple execve + * calls to review (only possible when using the shell_zsh_fork feature) + * - stdin reviews, which refer to the existing parent command item and + * have a separate approval ID in the action payload + * - network policy reviews, where there is no target item + * + * A network call is triggered by a CommandExecution item, so having a + * target_item_id set to the CommandExecution item would be misleading + * because the review is about the network call, not the command execution. + * Therefore, target_item_id is set to None for network policy reviews. + */ + targetItemId: string | null; + review: GuardianApprovalReview; + action: GuardianApprovalReviewAction; +}; diff --git a/src/runtimes/openai/generated/v2/ItemStartedNotification.ts b/src/runtimes/openai/generated/v2/ItemStartedNotification.ts new file mode 100644 index 0000000..e7bbec6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ItemStartedNotification.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadItem } from "./ThreadItem"; + +export type ItemStartedNotification = { + item: ThreadItem; + threadId: string; + turnId: string; + /** + * Unix timestamp (in milliseconds) when this item lifecycle started. + */ + startedAtMs: number; +}; diff --git a/src/runtimes/openai/generated/v2/ListMcpServerStatusParams.ts b/src/runtimes/openai/generated/v2/ListMcpServerStatusParams.ts new file mode 100644 index 0000000..655eb3b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ListMcpServerStatusParams.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpServerStatusDetail } from "./McpServerStatusDetail"; + +export type ListMcpServerStatusParams = { + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size; defaults to a server-defined value. + */ + limit?: number | null; + /** + * Controls how much MCP inventory data to fetch for each server. + * Defaults to `Full` when omitted. + */ + detail?: McpServerStatusDetail | null; + threadId?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ListMcpServerStatusResponse.ts b/src/runtimes/openai/generated/v2/ListMcpServerStatusResponse.ts new file mode 100644 index 0000000..9e08e03 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ListMcpServerStatusResponse.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpServerStatus } from "./McpServerStatus"; + +export type ListMcpServerStatusResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * If None, there are no more items to return. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/LoginAccountParams.ts b/src/runtimes/openai/generated/v2/LoginAccountParams.ts new file mode 100644 index 0000000..fd5964e --- /dev/null +++ b/src/runtimes/openai/generated/v2/LoginAccountParams.ts @@ -0,0 +1,41 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LoginAppBrand } from "./LoginAppBrand"; + +export type LoginAccountParams = + | { type: "apiKey"; apiKey: string } + | { + type: "chatgpt"; + codexStreamlinedLogin?: boolean; + useHostedLoginSuccessPage?: boolean; + appBrand?: LoginAppBrand | null; + } + | { type: "chatgptDeviceCode" } + | { + type: "chatgptAuthTokens"; + /** + * Access token (JWT) supplied by the client. + * This token is used for backend API requests and email extraction. + */ + accessToken: string; + /** + * Workspace/account identifier supplied by the client. + */ + chatgptAccountId: string; + /** + * Optional plan type supplied by the client. + * + * When `null`, Codex attempts to derive the plan type from access-token + * claims. If unavailable, the plan defaults to `unknown`. + */ + chatgptPlanType?: string | null; + } + | { type: "amazonBedrock"; apiKey: string; region: string } + | { + type: "amazonBedrockAccessKeys"; + accessKeyId: string; + secretAccessKey: string; + sessionToken?: string | null; + region: string; + }; diff --git a/src/runtimes/openai/generated/v2/LoginAccountResponse.ts b/src/runtimes/openai/generated/v2/LoginAccountResponse.ts new file mode 100644 index 0000000..5d195a6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/LoginAccountResponse.ts @@ -0,0 +1,28 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type LoginAccountResponse = + | { type: "apiKey" } + | { + type: "chatgpt"; + loginId: string; + /** + * URL the client should open in a browser to initiate the OAuth flow. + */ + authUrl: string; + } + | { + type: "chatgptDeviceCode"; + loginId: string; + /** + * URL the client should open in a browser to complete device code authorization. + */ + verificationUrl: string; + /** + * One-time code the user must enter after signing in. + */ + userCode: string; + } + | { type: "chatgptAuthTokens" } + | { type: "amazonBedrock" }; diff --git a/src/runtimes/openai/generated/v2/LoginAppBrand.ts b/src/runtimes/openai/generated/v2/LoginAppBrand.ts new file mode 100644 index 0000000..c06d12f --- /dev/null +++ b/src/runtimes/openai/generated/v2/LoginAppBrand.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type LoginAppBrand = "codex" | "chatgpt"; diff --git a/src/runtimes/openai/generated/v2/LogoutAccountResponse.ts b/src/runtimes/openai/generated/v2/LogoutAccountResponse.ts new file mode 100644 index 0000000..ec85cf0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/LogoutAccountResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type LogoutAccountResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ManagedHooksRequirements.ts b/src/runtimes/openai/generated/v2/ManagedHooksRequirements.ts new file mode 100644 index 0000000..431afdf --- /dev/null +++ b/src/runtimes/openai/generated/v2/ManagedHooksRequirements.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup"; + +export type ManagedHooksRequirements = { + managedDir: string | null; + windowsManagedDir: string | null; + PreToolUse: Array; + PermissionRequest: Array; + PostToolUse: Array; + PreCompact: Array; + PostCompact: Array; + SessionStart: Array; + SessionEnd: Array; + UserPromptSubmit: Array; + SubagentStart: Array; + SubagentStop: Array; + Stop: Array; + Interrupt: Array; +}; diff --git a/src/runtimes/openai/generated/v2/MarketplaceAddParams.ts b/src/runtimes/openai/generated/v2/MarketplaceAddParams.ts new file mode 100644 index 0000000..40f5378 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MarketplaceAddParams.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MarketplaceAddParams = { + source: string; + refName?: string | null; + sparsePaths?: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/MarketplaceAddResponse.ts b/src/runtimes/openai/generated/v2/MarketplaceAddResponse.ts new file mode 100644 index 0000000..96ca042 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MarketplaceAddResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type MarketplaceAddResponse = { + marketplaceName: string; + installedRoot: AbsolutePathBuf; + alreadyAdded: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/MarketplaceInterface.ts b/src/runtimes/openai/generated/v2/MarketplaceInterface.ts new file mode 100644 index 0000000..71f55eb --- /dev/null +++ b/src/runtimes/openai/generated/v2/MarketplaceInterface.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MarketplaceInterface = { displayName: string | null }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceLoadErrorInfo.ts b/src/runtimes/openai/generated/v2/MarketplaceLoadErrorInfo.ts new file mode 100644 index 0000000..15debd8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MarketplaceLoadErrorInfo.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type MarketplaceLoadErrorInfo = { marketplacePath: AbsolutePathBuf; message: string }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceRemoveParams.ts b/src/runtimes/openai/generated/v2/MarketplaceRemoveParams.ts new file mode 100644 index 0000000..88e6f3f --- /dev/null +++ b/src/runtimes/openai/generated/v2/MarketplaceRemoveParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MarketplaceRemoveParams = { marketplaceName: string }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceRemoveResponse.ts b/src/runtimes/openai/generated/v2/MarketplaceRemoveResponse.ts new file mode 100644 index 0000000..4ff8926 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MarketplaceRemoveResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type MarketplaceRemoveResponse = { + marketplaceName: string; + installedRoot: AbsolutePathBuf | null; +}; diff --git a/src/runtimes/openai/generated/v2/MarketplaceUpgradeErrorInfo.ts b/src/runtimes/openai/generated/v2/MarketplaceUpgradeErrorInfo.ts new file mode 100644 index 0000000..6f3dfc6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MarketplaceUpgradeErrorInfo.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MarketplaceUpgradeErrorInfo = { marketplaceName: string; message: string }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceUpgradeParams.ts b/src/runtimes/openai/generated/v2/MarketplaceUpgradeParams.ts new file mode 100644 index 0000000..2061385 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MarketplaceUpgradeParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MarketplaceUpgradeParams = { marketplaceName?: string | null }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceUpgradeResponse.ts b/src/runtimes/openai/generated/v2/MarketplaceUpgradeResponse.ts new file mode 100644 index 0000000..2affee4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MarketplaceUpgradeResponse.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo"; + +export type MarketplaceUpgradeResponse = { + selectedMarketplaces: Array; + upgradedRoots: Array; + errors: Array; +}; diff --git a/src/runtimes/openai/generated/v2/McpAuthStatus.ts b/src/runtimes/openai/generated/v2/McpAuthStatus.ts new file mode 100644 index 0000000..67d1233 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpAuthStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpAuthStatus = "unknown" | "unsupported" | "notLoggedIn" | "bearerToken" | "oAuth"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationArrayType.ts b/src/runtimes/openai/generated/v2/McpElicitationArrayType.ts new file mode 100644 index 0000000..066b44e --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationArrayType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpElicitationArrayType = "array"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationBooleanSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationBooleanSchema.ts new file mode 100644 index 0000000..3b77010 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationBooleanSchema.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationBooleanType } from "./McpElicitationBooleanType"; + +export type McpElicitationBooleanSchema = { + type: McpElicitationBooleanType; + title?: string; + description?: string; + default?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationBooleanType.ts b/src/runtimes/openai/generated/v2/McpElicitationBooleanType.ts new file mode 100644 index 0000000..f2b9ed4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationBooleanType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpElicitationBooleanType = "boolean"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationConstOption.ts b/src/runtimes/openai/generated/v2/McpElicitationConstOption.ts new file mode 100644 index 0000000..eed92e2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationConstOption.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpElicitationConstOption = { const: string; title: string }; diff --git a/src/runtimes/openai/generated/v2/McpElicitationEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationEnumSchema.ts new file mode 100644 index 0000000..2b2f7ba --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationEnumSchema.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationLegacyTitledEnumSchema } from "./McpElicitationLegacyTitledEnumSchema"; +import type { McpElicitationMultiSelectEnumSchema } from "./McpElicitationMultiSelectEnumSchema"; +import type { McpElicitationSingleSelectEnumSchema } from "./McpElicitationSingleSelectEnumSchema"; + +export type McpElicitationEnumSchema = + | McpElicitationSingleSelectEnumSchema + | McpElicitationMultiSelectEnumSchema + | McpElicitationLegacyTitledEnumSchema; diff --git a/src/runtimes/openai/generated/v2/McpElicitationLegacyTitledEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationLegacyTitledEnumSchema.ts new file mode 100644 index 0000000..cb43bcb --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationLegacyTitledEnumSchema.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationStringType } from "./McpElicitationStringType"; + +export type McpElicitationLegacyTitledEnumSchema = { + type: McpElicitationStringType; + title?: string; + description?: string; + enum: Array; + enumNames?: Array; + default?: string; +}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationMultiSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationMultiSelectEnumSchema.ts new file mode 100644 index 0000000..f99a19b --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationMultiSelectEnumSchema.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationTitledMultiSelectEnumSchema } from "./McpElicitationTitledMultiSelectEnumSchema"; +import type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema"; + +export type McpElicitationMultiSelectEnumSchema = + | McpElicitationUntitledMultiSelectEnumSchema + | McpElicitationTitledMultiSelectEnumSchema; diff --git a/src/runtimes/openai/generated/v2/McpElicitationNumberSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationNumberSchema.ts new file mode 100644 index 0000000..8cd9455 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationNumberSchema.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationNumberType } from "./McpElicitationNumberType"; + +export type McpElicitationNumberSchema = { + type: McpElicitationNumberType; + title?: string; + description?: string; + minimum?: number; + maximum?: number; + default?: number; +}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationNumberType.ts b/src/runtimes/openai/generated/v2/McpElicitationNumberType.ts new file mode 100644 index 0000000..96a9ded --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationNumberType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpElicitationNumberType = "number" | "integer"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationObjectType.ts b/src/runtimes/openai/generated/v2/McpElicitationObjectType.ts new file mode 100644 index 0000000..2449a0c --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationObjectType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpElicitationObjectType = "object"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationPrimitiveSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationPrimitiveSchema.ts new file mode 100644 index 0000000..a8b6668 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationPrimitiveSchema.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema"; +import type { McpElicitationEnumSchema } from "./McpElicitationEnumSchema"; +import type { McpElicitationNumberSchema } from "./McpElicitationNumberSchema"; +import type { McpElicitationStringSchema } from "./McpElicitationStringSchema"; + +export type McpElicitationPrimitiveSchema = + | McpElicitationEnumSchema + | McpElicitationStringSchema + | McpElicitationNumberSchema + | McpElicitationBooleanSchema; diff --git a/src/runtimes/openai/generated/v2/McpElicitationSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationSchema.ts new file mode 100644 index 0000000..34f703a --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationSchema.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationObjectType } from "./McpElicitationObjectType"; +import type { McpElicitationPrimitiveSchema } from "./McpElicitationPrimitiveSchema"; + +/** + * Typed form schema for MCP `elicitation/create` requests. + * + * This matches the `requestedSchema` shape from the MCP 2025-11-25 + * `ElicitRequestFormParams` schema. + */ +export type McpElicitationSchema = { + $schema?: string; + type: McpElicitationObjectType; + properties: { [key in string]?: McpElicitationPrimitiveSchema }; + required?: Array; +}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationSingleSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationSingleSelectEnumSchema.ts new file mode 100644 index 0000000..3d1ce01 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationSingleSelectEnumSchema.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitationTitledSingleSelectEnumSchema"; +import type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema"; + +export type McpElicitationSingleSelectEnumSchema = + | McpElicitationUntitledSingleSelectEnumSchema + | McpElicitationTitledSingleSelectEnumSchema; diff --git a/src/runtimes/openai/generated/v2/McpElicitationStringFormat.ts b/src/runtimes/openai/generated/v2/McpElicitationStringFormat.ts new file mode 100644 index 0000000..9891d4c --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationStringFormat.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpElicitationStringFormat = "email" | "uri" | "date" | "date-time"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationStringSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationStringSchema.ts new file mode 100644 index 0000000..5161f23 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationStringSchema.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationStringFormat } from "./McpElicitationStringFormat"; +import type { McpElicitationStringType } from "./McpElicitationStringType"; + +export type McpElicitationStringSchema = { + type: McpElicitationStringType; + title?: string; + description?: string; + minLength?: number; + maxLength?: number; + format?: McpElicitationStringFormat; + default?: string; +}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationStringType.ts b/src/runtimes/openai/generated/v2/McpElicitationStringType.ts new file mode 100644 index 0000000..bf2ddfa --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationStringType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpElicitationStringType = "string"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationTitledEnumItems.ts b/src/runtimes/openai/generated/v2/McpElicitationTitledEnumItems.ts new file mode 100644 index 0000000..3b6bab9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationTitledEnumItems.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationConstOption } from "./McpElicitationConstOption"; + +export type McpElicitationTitledEnumItems = { anyOf: Array }; diff --git a/src/runtimes/openai/generated/v2/McpElicitationTitledMultiSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationTitledMultiSelectEnumSchema.ts new file mode 100644 index 0000000..5128669 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationTitledMultiSelectEnumSchema.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationArrayType } from "./McpElicitationArrayType"; +import type { McpElicitationTitledEnumItems } from "./McpElicitationTitledEnumItems"; + +export type McpElicitationTitledMultiSelectEnumSchema = { + type: McpElicitationArrayType; + title?: string; + description?: string; + minItems?: bigint; + maxItems?: bigint; + items: McpElicitationTitledEnumItems; + default?: Array; +}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationTitledSingleSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationTitledSingleSelectEnumSchema.ts new file mode 100644 index 0000000..af4dcc8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationTitledSingleSelectEnumSchema.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationConstOption } from "./McpElicitationConstOption"; +import type { McpElicitationStringType } from "./McpElicitationStringType"; + +export type McpElicitationTitledSingleSelectEnumSchema = { + type: McpElicitationStringType; + title?: string; + description?: string; + oneOf: Array; + default?: string; +}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationUntitledEnumItems.ts b/src/runtimes/openai/generated/v2/McpElicitationUntitledEnumItems.ts new file mode 100644 index 0000000..0e33a9b --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationUntitledEnumItems.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationStringType } from "./McpElicitationStringType"; + +export type McpElicitationUntitledEnumItems = { + type: McpElicitationStringType; + enum: Array; +}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationUntitledMultiSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationUntitledMultiSelectEnumSchema.ts new file mode 100644 index 0000000..b5fd30e --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationUntitledMultiSelectEnumSchema.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationArrayType } from "./McpElicitationArrayType"; +import type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems"; + +export type McpElicitationUntitledMultiSelectEnumSchema = { + type: McpElicitationArrayType; + title?: string; + description?: string; + minItems?: bigint; + maxItems?: bigint; + items: McpElicitationUntitledEnumItems; + default?: Array; +}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationUntitledSingleSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationUntitledSingleSelectEnumSchema.ts new file mode 100644 index 0000000..65608a9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpElicitationUntitledSingleSelectEnumSchema.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpElicitationStringType } from "./McpElicitationStringType"; + +export type McpElicitationUntitledSingleSelectEnumSchema = { + type: McpElicitationStringType; + title?: string; + description?: string; + enum: Array; + default?: string; +}; diff --git a/src/runtimes/openai/generated/v2/McpResourceReadParams.ts b/src/runtimes/openai/generated/v2/McpResourceReadParams.ts new file mode 100644 index 0000000..b173fb2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpResourceReadParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpResourceReadParams = { + threadId?: string | null; + /** + * Originating MCP tool call used to select the resource's app. + */ + originCallId?: string | null; + server: string; + uri: string; + connectorId?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/McpResourceReadResponse.ts b/src/runtimes/openai/generated/v2/McpResourceReadResponse.ts new file mode 100644 index 0000000..c65eb79 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpResourceReadResponse.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ResourceContent } from "../ResourceContent"; + +export type McpResourceReadResponse = { + contents: Array; + /** + * Originating call when the server applied app-specific resource scoping. + */ + originCallId: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/McpServerConnectionStatus.ts b/src/runtimes/openai/generated/v2/McpServerConnectionStatus.ts new file mode 100644 index 0000000..df1912a --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerConnectionStatus.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerConnectionStatus = + | "notStarted" + | "starting" + | "connected" + | "authenticationRequired" + | "failed" + | "cancelled" + | "disabled"; diff --git a/src/runtimes/openai/generated/v2/McpServerElicitationAction.ts b/src/runtimes/openai/generated/v2/McpServerElicitationAction.ts new file mode 100644 index 0000000..7be134c --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerElicitationAction.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerElicitationAction = "accept" | "decline" | "cancel"; diff --git a/src/runtimes/openai/generated/v2/McpServerElicitationRequestParams.ts b/src/runtimes/openai/generated/v2/McpServerElicitationRequestParams.ts new file mode 100644 index 0000000..0080563 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerElicitationRequestParams.ts @@ -0,0 +1,28 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; +import type { McpElicitationSchema } from "./McpElicitationSchema"; + +export type McpServerElicitationRequestParams = { + threadId: string; + /** + * Active Codex turn when this elicitation was observed, if app-server could correlate one. + * + * This is nullable because MCP models elicitation as a standalone server-to-client request + * identified by the MCP server request id. It may be triggered during a turn, but turn + * context is app-server correlation rather than part of the protocol identity of the + * elicitation itself. + */ + turnId: string | null; + serverName: string; +} & ( + | { + mode: "form"; + _meta: JsonValue | null; + message: string; + requestedSchema: McpElicitationSchema; + } + | { mode: "openai/form"; _meta: JsonValue | null; message: string; requestedSchema: JsonValue } + | { mode: "url"; _meta: JsonValue | null; message: string; url: string; elicitationId: string } +); diff --git a/src/runtimes/openai/generated/v2/McpServerElicitationRequestResponse.ts b/src/runtimes/openai/generated/v2/McpServerElicitationRequestResponse.ts new file mode 100644 index 0000000..e75b7cc --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerElicitationRequestResponse.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; +import type { McpServerElicitationAction } from "./McpServerElicitationAction"; + +export type McpServerElicitationRequestResponse = { + action: McpServerElicitationAction; + /** + * Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`. + * + * This is nullable because decline/cancel responses have no content. + */ + content: JsonValue | null; + /** + * Optional client metadata for form-mode action handling. + */ + _meta: JsonValue | null; +}; diff --git a/src/runtimes/openai/generated/v2/McpServerEventNotification.ts b/src/runtimes/openai/generated/v2/McpServerEventNotification.ts new file mode 100644 index 0000000..5fa1dac --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerEventNotification.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type McpServerEventNotification = { method: string; params: JsonValue }; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamNotification.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamNotification.ts new file mode 100644 index 0000000..188f94f --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerEventStreamNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpServerEventNotification } from "./McpServerEventNotification"; + +export type McpServerEventStreamNotification = { + subscriptionId: string; + notification: McpServerEventNotification; +}; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamStartParams.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamStartParams.ts new file mode 100644 index 0000000..9f213e2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerEventStreamStartParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type McpServerEventStreamStartParams = { + threadId: string; + server: string; + subscriptionId: string; + name: string; + arguments: JsonValue; + _meta?: JsonValue | null; +}; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamStartResponse.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamStartResponse.ts new file mode 100644 index 0000000..382bc67 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerEventStreamStartResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerEventStreamStartResponse = Record; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamStopParams.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamStopParams.ts new file mode 100644 index 0000000..2a6c063 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerEventStreamStopParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerEventStreamStopParams = { subscriptionId: string }; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamStopResponse.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamStopResponse.ts new file mode 100644 index 0000000..9e9c2e9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerEventStreamStopResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerEventStreamStopResponse = Record; diff --git a/src/runtimes/openai/generated/v2/McpServerMigration.ts b/src/runtimes/openai/generated/v2/McpServerMigration.ts new file mode 100644 index 0000000..fe7f200 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerMigration.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/McpServerOauthClientRegistration.ts b/src/runtimes/openai/generated/v2/McpServerOauthClientRegistration.ts new file mode 100644 index 0000000..052150b --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerOauthClientRegistration.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerOauthClientRegistration = "auto" | "cimd" | "dcr"; diff --git a/src/runtimes/openai/generated/v2/McpServerOauthLoginCompletedNotification.ts b/src/runtimes/openai/generated/v2/McpServerOauthLoginCompletedNotification.ts new file mode 100644 index 0000000..322e219 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerOauthLoginCompletedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerOauthLoginCompletedNotification = { + name: string; + threadId: string | null; + success: boolean; + error?: string; +}; diff --git a/src/runtimes/openai/generated/v2/McpServerOauthLoginParams.ts b/src/runtimes/openai/generated/v2/McpServerOauthLoginParams.ts new file mode 100644 index 0000000..6e8dc7e --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerOauthLoginParams.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpServerOauthClientRegistration } from "./McpServerOauthClientRegistration"; + +export type McpServerOauthLoginParams = { + name: string; + threadId?: string | null; + /** + * Registration strategy for this login only; omission selects automatic discovery. + */ + clientRegistration?: McpServerOauthClientRegistration | null; + scopes?: Array | null; + timeoutSecs?: bigint | null; +}; diff --git a/src/runtimes/openai/generated/v2/McpServerOauthLoginResponse.ts b/src/runtimes/openai/generated/v2/McpServerOauthLoginResponse.ts new file mode 100644 index 0000000..b119e07 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerOauthLoginResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerOauthLoginResponse = { authorizationUrl: string }; diff --git a/src/runtimes/openai/generated/v2/McpServerRefreshResponse.ts b/src/runtimes/openai/generated/v2/McpServerRefreshResponse.ts new file mode 100644 index 0000000..48a25d2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerRefreshResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerRefreshResponse = Record; diff --git a/src/runtimes/openai/generated/v2/McpServerStartupFailureReason.ts b/src/runtimes/openai/generated/v2/McpServerStartupFailureReason.ts new file mode 100644 index 0000000..0373e54 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerStartupFailureReason.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerStartupFailureReason = "reauthenticationRequired"; diff --git a/src/runtimes/openai/generated/v2/McpServerStartupState.ts b/src/runtimes/openai/generated/v2/McpServerStartupState.ts new file mode 100644 index 0000000..c62babc --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerStartupState.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerStartupState = "starting" | "ready" | "failed" | "cancelled"; diff --git a/src/runtimes/openai/generated/v2/McpServerStatus.ts b/src/runtimes/openai/generated/v2/McpServerStatus.ts new file mode 100644 index 0000000..98a039c --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerStatus.ts @@ -0,0 +1,23 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpServerInfo } from "../McpServerInfo"; +import type { Resource } from "../Resource"; +import type { ResourceTemplate } from "../ResourceTemplate"; +import type { Tool } from "../Tool"; +import type { McpAuthStatus } from "./McpAuthStatus"; +import type { McpServerConnectionStatus } from "./McpServerConnectionStatus"; + +export type McpServerStatus = { + name: string; + /** + * Current thread-runtime connection state; null when unavailable or the configuration changed. + */ + runtimeStatus: McpServerConnectionStatus | null; + pluginId: string | null; + serverInfo: McpServerInfo | null; + tools: { [key in string]?: Tool }; + resources: Array; + resourceTemplates: Array; + authStatus: McpAuthStatus; +}; diff --git a/src/runtimes/openai/generated/v2/McpServerStatusDetail.ts b/src/runtimes/openai/generated/v2/McpServerStatusDetail.ts new file mode 100644 index 0000000..ab97cc2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerStatusDetail.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerStatusDetail = "full" | "toolsAndAuthOnly"; diff --git a/src/runtimes/openai/generated/v2/McpServerStatusUpdatedNotification.ts b/src/runtimes/openai/generated/v2/McpServerStatusUpdatedNotification.ts new file mode 100644 index 0000000..b439318 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerStatusUpdatedNotification.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpServerStartupFailureReason } from "./McpServerStartupFailureReason"; +import type { McpServerStartupState } from "./McpServerStartupState"; + +export type McpServerStatusUpdatedNotification = { + threadId: string | null; + name: string; + status: McpServerStartupState; + error: string | null; + failureReason: McpServerStartupFailureReason | null; +}; diff --git a/src/runtimes/openai/generated/v2/McpServerToolCallParams.ts b/src/runtimes/openai/generated/v2/McpServerToolCallParams.ts new file mode 100644 index 0000000..7bff8e2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerToolCallParams.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type McpServerToolCallParams = { + threadId: string; + server: string; + tool: string; + arguments?: JsonValue; + _meta?: JsonValue; +}; diff --git a/src/runtimes/openai/generated/v2/McpServerToolCallResponse.ts b/src/runtimes/openai/generated/v2/McpServerToolCallResponse.ts new file mode 100644 index 0000000..75c9de1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpServerToolCallResponse.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type McpServerToolCallResponse = { + content: Array; + structuredContent?: JsonValue; + isError?: boolean; + _meta?: JsonValue; +}; diff --git a/src/runtimes/openai/generated/v2/McpToolCallAppContext.ts b/src/runtimes/openai/generated/v2/McpToolCallAppContext.ts new file mode 100644 index 0000000..9101810 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpToolCallAppContext.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpToolCallAppContext = { + connectorId: string; + linkId: string | null; + resourceUri: string | null; + appName: string | null; + actionName: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/McpToolCallError.ts b/src/runtimes/openai/generated/v2/McpToolCallError.ts new file mode 100644 index 0000000..f11054a --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpToolCallError.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpToolCallError = { message: string }; diff --git a/src/runtimes/openai/generated/v2/McpToolCallProgressNotification.ts b/src/runtimes/openai/generated/v2/McpToolCallProgressNotification.ts new file mode 100644 index 0000000..3f3137e --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpToolCallProgressNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpToolCallProgressNotification = { + threadId: string; + turnId: string; + itemId: string; + message: string; +}; diff --git a/src/runtimes/openai/generated/v2/McpToolCallResult.ts b/src/runtimes/openai/generated/v2/McpToolCallResult.ts new file mode 100644 index 0000000..4bbaa31 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpToolCallResult.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type McpToolCallResult = { + content: Array; + structuredContent: JsonValue | null; + _meta: JsonValue | null; +}; diff --git a/src/runtimes/openai/generated/v2/McpToolCallStatus.ts b/src/runtimes/openai/generated/v2/McpToolCallStatus.ts new file mode 100644 index 0000000..f46bca0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/McpToolCallStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpToolCallStatus = "inProgress" | "completed" | "failed"; diff --git a/src/runtimes/openai/generated/v2/MemoryCitation.ts b/src/runtimes/openai/generated/v2/MemoryCitation.ts new file mode 100644 index 0000000..ffc652f --- /dev/null +++ b/src/runtimes/openai/generated/v2/MemoryCitation.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MemoryCitationEntry } from "./MemoryCitationEntry"; + +export type MemoryCitation = { entries: Array; threadIds: Array }; diff --git a/src/runtimes/openai/generated/v2/MemoryCitationEntry.ts b/src/runtimes/openai/generated/v2/MemoryCitationEntry.ts new file mode 100644 index 0000000..eb74521 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MemoryCitationEntry.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MemoryCitationEntry = { + path: string; + lineStart: number; + lineEnd: number; + note: string; +}; diff --git a/src/runtimes/openai/generated/v2/MemoryResetResponse.ts b/src/runtimes/openai/generated/v2/MemoryResetResponse.ts new file mode 100644 index 0000000..d950794 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MemoryResetResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MemoryResetResponse = Record; diff --git a/src/runtimes/openai/generated/v2/MergeStrategy.ts b/src/runtimes/openai/generated/v2/MergeStrategy.ts new file mode 100644 index 0000000..098677f --- /dev/null +++ b/src/runtimes/openai/generated/v2/MergeStrategy.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MergeStrategy = "replace" | "upsert"; diff --git a/src/runtimes/openai/generated/v2/MigrationDetails.ts b/src/runtimes/openai/generated/v2/MigrationDetails.ts new file mode 100644 index 0000000..cec07e8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MigrationDetails.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CommandMigration } from "./CommandMigration"; +import type { HookMigration } from "./HookMigration"; +import type { McpServerMigration } from "./McpServerMigration"; +import type { PluginsMigration } from "./PluginsMigration"; +import type { SessionMigration } from "./SessionMigration"; +import type { SkillMigration } from "./SkillMigration"; +import type { SubagentMigration } from "./SubagentMigration"; + +export type MigrationDetails = { + plugins: Array; + skills: Array; + sessions: Array; + mcpServers: Array; + hooks: Array; + subagents: Array; + commands: Array; + memory?: Array; +}; diff --git a/src/runtimes/openai/generated/v2/MockExperimentalMethodParams.ts b/src/runtimes/openai/generated/v2/MockExperimentalMethodParams.ts new file mode 100644 index 0000000..f48f796 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MockExperimentalMethodParams.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MockExperimentalMethodParams = { + /** + * Test-only payload field. + */ + value?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/MockExperimentalMethodResponse.ts b/src/runtimes/openai/generated/v2/MockExperimentalMethodResponse.ts new file mode 100644 index 0000000..02ad930 --- /dev/null +++ b/src/runtimes/openai/generated/v2/MockExperimentalMethodResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MockExperimentalMethodResponse = { + /** + * Echoes the input `value`. + */ + echoed: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/Model.ts b/src/runtimes/openai/generated/v2/Model.ts new file mode 100644 index 0000000..f945e53 --- /dev/null +++ b/src/runtimes/openai/generated/v2/Model.ts @@ -0,0 +1,40 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InputModality } from "../InputModality"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; +import type { ModelServiceTier } from "./ModelServiceTier"; +import type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; +import type { MultiAgentVersion } from "./MultiAgentVersion"; +import type { ReasoningEffortOption } from "./ReasoningEffortOption"; + +export type Model = { + id: string; + model: string; + upgrade: string | null; + upgradeInfo: ModelUpgradeInfo | null; + availabilityNux: ModelAvailabilityNux | null; + displayName: string; + description: string; + modelSpecialty: string | null; + hidden: boolean; + supportedReasoningEfforts: Array; + defaultReasoningEffort: ReasoningEffort; + inputModalities: Array; + supportsPersonality: boolean; + /** + * Multi-agent runtime declared by this model, when available. + */ + multiAgentVersion: MultiAgentVersion | null; + /** + * Deprecated: use `serviceTiers` instead. + */ + additionalSpeedTiers: Array; + serviceTiers: Array; + /** + * Catalog default service tier id for this model, when one is configured. + */ + defaultServiceTier: string | null; + isDefault: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ModelAvailabilityNux.ts b/src/runtimes/openai/generated/v2/ModelAvailabilityNux.ts new file mode 100644 index 0000000..8ea4b6e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelAvailabilityNux.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelAvailabilityNux = { message: string }; diff --git a/src/runtimes/openai/generated/v2/ModelListParams.ts b/src/runtimes/openai/generated/v2/ModelListParams.ts new file mode 100644 index 0000000..568c8ae --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelListParams.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelListParams = { + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size; defaults to a reasonable server-side value. + */ + limit?: number | null; + /** + * When true, include models that are hidden from the default picker list. + */ + includeHidden?: boolean | null; +}; diff --git a/src/runtimes/openai/generated/v2/ModelListResponse.ts b/src/runtimes/openai/generated/v2/ModelListResponse.ts new file mode 100644 index 0000000..ad5077a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelListResponse.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Model } from "./Model"; + +export type ModelListResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * If None, there are no more items to return. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadParams.ts b/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadParams.ts new file mode 100644 index 0000000..00cbe47 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelProviderCapabilitiesReadParams = Record; diff --git a/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadResponse.ts b/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadResponse.ts new file mode 100644 index 0000000..f831613 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelProviderCapabilitiesReadResponse = { + namespaceTools: boolean; + imageGeneration: boolean; + webSearch: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ModelRerouteReason.ts b/src/runtimes/openai/generated/v2/ModelRerouteReason.ts new file mode 100644 index 0000000..e780e7f --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelRerouteReason.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelRerouteReason = "highRiskCyberActivity"; diff --git a/src/runtimes/openai/generated/v2/ModelReroutedNotification.ts b/src/runtimes/openai/generated/v2/ModelReroutedNotification.ts new file mode 100644 index 0000000..3a720a9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelReroutedNotification.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ModelRerouteReason } from "./ModelRerouteReason"; + +export type ModelReroutedNotification = { + threadId: string; + turnId: string; + fromModel: string; + toModel: string; + reason: ModelRerouteReason; +}; diff --git a/src/runtimes/openai/generated/v2/ModelSafetyBufferingUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ModelSafetyBufferingUpdatedNotification.ts new file mode 100644 index 0000000..315d913 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelSafetyBufferingUpdatedNotification.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelSafetyBufferingUpdatedNotification = { + threadId: string; + turnId: string; + model: string; + useCases: Array; + reasons: Array; + showBufferingUi: boolean; + fasterModel: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ModelServiceTier.ts b/src/runtimes/openai/generated/v2/ModelServiceTier.ts new file mode 100644 index 0000000..b2a8359 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelServiceTier.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelServiceTier = { id: string; name: string; description: string }; diff --git a/src/runtimes/openai/generated/v2/ModelUpgradeInfo.ts b/src/runtimes/openai/generated/v2/ModelUpgradeInfo.ts new file mode 100644 index 0000000..957820d --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelUpgradeInfo.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelUpgradeInfo = { + model: string; + upgradeCopy: string | null; + modelLink: string | null; + migrationMarkdown: string | null; + /** + * Informational Unix timestamp for this upgrade's scheduled retirement, if known. + */ + retirementAt: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ModelVerification.ts b/src/runtimes/openai/generated/v2/ModelVerification.ts new file mode 100644 index 0000000..00538c0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelVerification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelVerification = "trustedAccessForCyber"; diff --git a/src/runtimes/openai/generated/v2/ModelVerificationNotification.ts b/src/runtimes/openai/generated/v2/ModelVerificationNotification.ts new file mode 100644 index 0000000..d328ae3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelVerificationNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ModelVerification } from "./ModelVerification"; + +export type ModelVerificationNotification = { + threadId: string; + turnId: string; + verifications: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ModelsRequirements.ts b/src/runtimes/openai/generated/v2/ModelsRequirements.ts new file mode 100644 index 0000000..ed828da --- /dev/null +++ b/src/runtimes/openai/generated/v2/ModelsRequirements.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NewThreadModelDefaults } from "./NewThreadModelDefaults"; + +export type ModelsRequirements = { newThread: NewThreadModelDefaults | null }; diff --git a/src/runtimes/openai/generated/v2/MultiAgentVersion.ts b/src/runtimes/openai/generated/v2/MultiAgentVersion.ts new file mode 100644 index 0000000..b71323e --- /dev/null +++ b/src/runtimes/openai/generated/v2/MultiAgentVersion.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Multi-agent runtime supported by a model. + */ +export type MultiAgentVersion = "disabled" | "v1" | "v2"; diff --git a/src/runtimes/openai/generated/v2/NetworkAccess.ts b/src/runtimes/openai/generated/v2/NetworkAccess.ts new file mode 100644 index 0000000..7b697b2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/NetworkAccess.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkAccess = "restricted" | "enabled"; diff --git a/src/runtimes/openai/generated/v2/NetworkApprovalContext.ts b/src/runtimes/openai/generated/v2/NetworkApprovalContext.ts new file mode 100644 index 0000000..ef7ab28 --- /dev/null +++ b/src/runtimes/openai/generated/v2/NetworkApprovalContext.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; + +export type NetworkApprovalContext = { host: string; protocol: NetworkApprovalProtocol }; diff --git a/src/runtimes/openai/generated/v2/NetworkApprovalProtocol.ts b/src/runtimes/openai/generated/v2/NetworkApprovalProtocol.ts new file mode 100644 index 0000000..9dd4066 --- /dev/null +++ b/src/runtimes/openai/generated/v2/NetworkApprovalProtocol.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkApprovalProtocol = "http" | "https" | "socks5Tcp" | "socks5Udp"; diff --git a/src/runtimes/openai/generated/v2/NetworkDomainPermission.ts b/src/runtimes/openai/generated/v2/NetworkDomainPermission.ts new file mode 100644 index 0000000..2ea4439 --- /dev/null +++ b/src/runtimes/openai/generated/v2/NetworkDomainPermission.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkDomainPermission = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/v2/NetworkPolicyAmendment.ts b/src/runtimes/openai/generated/v2/NetworkPolicyAmendment.ts new file mode 100644 index 0000000..51348de --- /dev/null +++ b/src/runtimes/openai/generated/v2/NetworkPolicyAmendment.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; + +export type NetworkPolicyAmendment = { host: string; action: NetworkPolicyRuleAction }; diff --git a/src/runtimes/openai/generated/v2/NetworkPolicyRuleAction.ts b/src/runtimes/openai/generated/v2/NetworkPolicyRuleAction.ts new file mode 100644 index 0000000..55ec700 --- /dev/null +++ b/src/runtimes/openai/generated/v2/NetworkPolicyRuleAction.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkPolicyRuleAction = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/v2/NetworkRequirements.ts b/src/runtimes/openai/generated/v2/NetworkRequirements.ts new file mode 100644 index 0000000..e88a0c1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/NetworkRequirements.ts @@ -0,0 +1,40 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NetworkDomainPermission } from "./NetworkDomainPermission"; +import type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission"; + +export type NetworkRequirements = { + enabled: boolean | null; + httpPort: number | null; + socksPort: number | null; + allowUpstreamProxy: boolean | null; + dangerouslyAllowNonLoopbackProxy: boolean | null; + dangerouslyAllowAllUnixSockets: boolean | null; + /** + * Canonical network permission map for `experimental_network`. + */ + domains: { [key in string]?: NetworkDomainPermission } | null; + /** + * When true, only managed allowlist entries are respected while managed + * network enforcement is active. + */ + managedAllowedDomainsOnly: boolean | null; + /** + * Legacy compatibility view derived from `domains`. + */ + allowedDomains: Array | null; + /** + * Legacy compatibility view derived from `domains`. + */ + deniedDomains: Array | null; + /** + * Canonical unix socket permission map for `experimental_network`. + */ + unixSockets: { [key in string]?: NetworkUnixSocketPermission } | null; + /** + * Legacy compatibility view derived from `unix_sockets`. + */ + allowUnixSockets: Array | null; + allowLocalBinding: boolean | null; +}; diff --git a/src/runtimes/openai/generated/v2/NetworkUnixSocketPermission.ts b/src/runtimes/openai/generated/v2/NetworkUnixSocketPermission.ts new file mode 100644 index 0000000..c5474cb --- /dev/null +++ b/src/runtimes/openai/generated/v2/NetworkUnixSocketPermission.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkUnixSocketPermission = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/v2/NewThreadModelDefaults.ts b/src/runtimes/openai/generated/v2/NewThreadModelDefaults.ts new file mode 100644 index 0000000..f664b37 --- /dev/null +++ b/src/runtimes/openai/generated/v2/NewThreadModelDefaults.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReasoningEffort } from "../ReasoningEffort"; + +export type NewThreadModelDefaults = { + model: string | null; + modelReasoningEffort: ReasoningEffort | null; + serviceTier: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/NonSteerableTurnKind.ts b/src/runtimes/openai/generated/v2/NonSteerableTurnKind.ts new file mode 100644 index 0000000..2624df2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/NonSteerableTurnKind.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NonSteerableTurnKind = "review" | "compact"; diff --git a/src/runtimes/openai/generated/v2/OverriddenMetadata.ts b/src/runtimes/openai/generated/v2/OverriddenMetadata.ts new file mode 100644 index 0000000..04fe461 --- /dev/null +++ b/src/runtimes/openai/generated/v2/OverriddenMetadata.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; +import type { ConfigLayerMetadata } from "./ConfigLayerMetadata"; + +export type OverriddenMetadata = { + message: string; + overridingLayer: ConfigLayerMetadata; + effectiveValue: JsonValue; +}; diff --git a/src/runtimes/openai/generated/v2/PatchApplyStatus.ts b/src/runtimes/openai/generated/v2/PatchApplyStatus.ts new file mode 100644 index 0000000..620be78 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PatchApplyStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PatchApplyStatus = "inProgress" | "completed" | "failed" | "declined"; diff --git a/src/runtimes/openai/generated/v2/PatchChangeKind.ts b/src/runtimes/openai/generated/v2/PatchChangeKind.ts new file mode 100644 index 0000000..41ef25c --- /dev/null +++ b/src/runtimes/openai/generated/v2/PatchChangeKind.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PatchChangeKind = + | { type: "add" } + | { type: "delete" } + | { type: "update"; move_path: string | null }; diff --git a/src/runtimes/openai/generated/v2/PermissionGrantScope.ts b/src/runtimes/openai/generated/v2/PermissionGrantScope.ts new file mode 100644 index 0000000..8ca127e --- /dev/null +++ b/src/runtimes/openai/generated/v2/PermissionGrantScope.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PermissionGrantScope = "turn" | "session"; diff --git a/src/runtimes/openai/generated/v2/PermissionProfileListParams.ts b/src/runtimes/openai/generated/v2/PermissionProfileListParams.ts new file mode 100644 index 0000000..151b39b --- /dev/null +++ b/src/runtimes/openai/generated/v2/PermissionProfileListParams.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PermissionProfileListParams = { + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size; defaults to the full result set. + */ + limit?: number | null; + /** + * Optional working directory to resolve project config layers. + */ + cwd?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/PermissionProfileListResponse.ts b/src/runtimes/openai/generated/v2/PermissionProfileListResponse.ts new file mode 100644 index 0000000..eade39b --- /dev/null +++ b/src/runtimes/openai/generated/v2/PermissionProfileListResponse.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PermissionProfileSummary } from "./PermissionProfileSummary"; + +export type PermissionProfileListResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * If None, there are no more items to return. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/PermissionProfileSummary.ts b/src/runtimes/openai/generated/v2/PermissionProfileSummary.ts new file mode 100644 index 0000000..d775dc6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PermissionProfileSummary.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PermissionProfileSummary = { + /** + * Available permission profile identifier. + */ + id: string; + /** + * Optional user-facing description for display in clients. + */ + description: string | null; + /** + * Whether the effective requirements allow selecting this profile. + */ + allowed: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/PermissionsRequestApprovalParams.ts b/src/runtimes/openai/generated/v2/PermissionsRequestApprovalParams.ts new file mode 100644 index 0000000..8d050ae --- /dev/null +++ b/src/runtimes/openai/generated/v2/PermissionsRequestApprovalParams.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { RequestPermissionProfile } from "./RequestPermissionProfile"; + +export type PermissionsRequestApprovalParams = { + threadId: string; + turnId: string; + itemId: string; + environmentId: string | null; + /** + * Unix timestamp (in milliseconds) when this approval request started. + */ + startedAtMs: number; + cwd: AbsolutePathBuf; + reason: string | null; + permissions: RequestPermissionProfile; +}; diff --git a/src/runtimes/openai/generated/v2/PermissionsRequestApprovalResponse.ts b/src/runtimes/openai/generated/v2/PermissionsRequestApprovalResponse.ts new file mode 100644 index 0000000..eb7d5ac --- /dev/null +++ b/src/runtimes/openai/generated/v2/PermissionsRequestApprovalResponse.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { GrantedPermissionProfile } from "./GrantedPermissionProfile"; +import type { PermissionGrantScope } from "./PermissionGrantScope"; + +export type PermissionsRequestApprovalResponse = { + permissions: GrantedPermissionProfile; + scope: PermissionGrantScope; + /** + * Review every subsequent command in this turn before normal sandboxed execution. + */ + strictAutoReview?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/PlanDeltaNotification.ts b/src/runtimes/openai/generated/v2/PlanDeltaNotification.ts new file mode 100644 index 0000000..262f652 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PlanDeltaNotification.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should + * not assume concatenated deltas match the completed plan item content. + */ +export type PlanDeltaNotification = { + threadId: string; + turnId: string; + itemId: string; + delta: string; +}; diff --git a/src/runtimes/openai/generated/v2/PluginAuthPolicy.ts b/src/runtimes/openai/generated/v2/PluginAuthPolicy.ts new file mode 100644 index 0000000..5b90e9c --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginAuthPolicy.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginAuthPolicy = "ON_INSTALL" | "ON_USE"; diff --git a/src/runtimes/openai/generated/v2/PluginAvailability.ts b/src/runtimes/openai/generated/v2/PluginAvailability.ts new file mode 100644 index 0000000..bec0b88 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginAvailability.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginAvailability = "AVAILABLE" | "DISABLED_BY_ADMIN"; diff --git a/src/runtimes/openai/generated/v2/PluginDetail.ts b/src/runtimes/openai/generated/v2/PluginDetail.ts new file mode 100644 index 0000000..37337cd --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginDetail.ts @@ -0,0 +1,24 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { AppSummary } from "./AppSummary"; +import type { AppTemplateSummary } from "./AppTemplateSummary"; +import type { PluginHookSummary } from "./PluginHookSummary"; +import type { PluginSummary } from "./PluginSummary"; +import type { ScheduledTaskSummary } from "./ScheduledTaskSummary"; +import type { SkillSummary } from "./SkillSummary"; + +export type PluginDetail = { + marketplaceName: string; + marketplacePath: AbsolutePathBuf | null; + summary: PluginSummary; + shareUrl: string | null; + description: string | null; + skills: Array; + hooks: Array; + apps: Array; + appTemplates: Array; + mcpServers: Array; + scheduledTasks: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/PluginDisabledReason.ts b/src/runtimes/openai/generated/v2/PluginDisabledReason.ts new file mode 100644 index 0000000..80eed0b --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginDisabledReason.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginDisabledReason = + | "disabled_by_admin" + | "plan_not_eligible" + | "required_app_unavailable" + | "unknown"; diff --git a/src/runtimes/openai/generated/v2/PluginHookSummary.ts b/src/runtimes/openai/generated/v2/PluginHookSummary.ts new file mode 100644 index 0000000..03fe54a --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginHookSummary.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HookEventName } from "./HookEventName"; + +export type PluginHookSummary = { key: string; eventName: HookEventName }; diff --git a/src/runtimes/openai/generated/v2/PluginInstallParams.ts b/src/runtimes/openai/generated/v2/PluginInstallParams.ts new file mode 100644 index 0000000..b8e6491 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginInstallParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type PluginInstallParams = { + marketplacePath?: AbsolutePathBuf | null; + remoteMarketplaceName?: string | null; + /** + * Client-generated identifier used to correlate one installation attempt. + */ + installAttemptId?: string | null; + pluginName: string; +}; diff --git a/src/runtimes/openai/generated/v2/PluginInstallPolicy.ts b/src/runtimes/openai/generated/v2/PluginInstallPolicy.ts new file mode 100644 index 0000000..d624f38 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginInstallPolicy.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginInstallPolicy = "NOT_AVAILABLE" | "AVAILABLE" | "INSTALLED_BY_DEFAULT"; diff --git a/src/runtimes/openai/generated/v2/PluginInstallPolicySource.ts b/src/runtimes/openai/generated/v2/PluginInstallPolicySource.ts new file mode 100644 index 0000000..caa3962 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginInstallPolicySource.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginInstallPolicySource = "WORKSPACE_SETTING" | "IMPLICIT_CANONICAL_APP"; diff --git a/src/runtimes/openai/generated/v2/PluginInstallResponse.ts b/src/runtimes/openai/generated/v2/PluginInstallResponse.ts new file mode 100644 index 0000000..af50cee --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginInstallResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppSummary } from "./AppSummary"; +import type { PluginAuthPolicy } from "./PluginAuthPolicy"; + +export type PluginInstallResponse = { + authPolicy: PluginAuthPolicy; + appsNeedingAuth: Array; +}; diff --git a/src/runtimes/openai/generated/v2/PluginInstalledParams.ts b/src/runtimes/openai/generated/v2/PluginInstalledParams.ts new file mode 100644 index 0000000..0a3b811 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginInstalledParams.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type PluginInstalledParams = { + /** + * Optional working directories used to discover repo marketplaces. + */ + cwds?: Array | null; + /** + * Additional uninstalled plugin names that should be returned when present locally. + * This is used by mention surfaces that intentionally expose install entrypoints. + */ + installSuggestionPluginNames?: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/PluginInstalledResponse.ts b/src/runtimes/openai/generated/v2/PluginInstalledResponse.ts new file mode 100644 index 0000000..470e694 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginInstalledResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo"; +import type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry"; + +export type PluginInstalledResponse = { + marketplaces: Array; + marketplaceLoadErrors: Array; +}; diff --git a/src/runtimes/openai/generated/v2/PluginInterface.ts b/src/runtimes/openai/generated/v2/PluginInterface.ts new file mode 100644 index 0000000..4cffe82 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginInterface.ts @@ -0,0 +1,54 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type PluginInterface = { + displayName: string | null; + shortDescription: string | null; + longDescription: string | null; + developerName: string | null; + category: string | null; + capabilities: Array; + websiteUrl: string | null; + privacyPolicyUrl: string | null; + termsOfServiceUrl: string | null; + /** + * Starter prompts for the plugin. Capped at 3 entries with a maximum of + * 128 characters per entry. + */ + defaultPrompt: Array | null; + brandColor: string | null; + /** + * Local composer icon path, resolved from the installed plugin package. + */ + composerIcon: AbsolutePathBuf | null; + /** + * Remote composer icon URL from the plugin catalog. + */ + composerIconUrl: string | null; + /** + * Local logo path, resolved from the installed plugin package. + */ + logo: AbsolutePathBuf | null; + /** + * Local dark-mode logo path, resolved from the installed plugin package. + */ + logoDark: AbsolutePathBuf | null; + /** + * Remote logo URL from the plugin catalog. + */ + logoUrl: string | null; + /** + * Remote dark-mode logo URL from the plugin catalog. + */ + logoUrlDark: string | null; + /** + * Local screenshot paths, resolved from the installed plugin package. + */ + screenshots: Array; + /** + * Remote screenshot URLs from the plugin catalog. + */ + screenshotUrls: Array; +}; diff --git a/src/runtimes/openai/generated/v2/PluginListMarketplaceKind.ts b/src/runtimes/openai/generated/v2/PluginListMarketplaceKind.ts new file mode 100644 index 0000000..0c39d2f --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginListMarketplaceKind.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginListMarketplaceKind = + | "local" + | "vertical" + | "workspace-directory" + | "shared-with-me" + | "created-by-me-remote"; diff --git a/src/runtimes/openai/generated/v2/PluginListParams.ts b/src/runtimes/openai/generated/v2/PluginListParams.ts new file mode 100644 index 0000000..96c6bf9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginListParams.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { PluginListMarketplaceKind } from "./PluginListMarketplaceKind"; + +export type PluginListParams = { + /** + * Optional working directories used to discover repo marketplaces. When omitted, + * only home-scoped marketplaces and the official curated marketplace are considered. + */ + cwds?: Array | null; + /** + * Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus + * the default remote catalog when enabled by feature flag. + */ + marketplaceKinds?: Array | null; + /** + * Whether the client requests a fresh remote plugin catalog fetch. + */ + forceRefetch?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/PluginListResponse.ts b/src/runtimes/openai/generated/v2/PluginListResponse.ts new file mode 100644 index 0000000..c4ecfca --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginListResponse.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo"; +import type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry"; + +export type PluginListResponse = { + marketplaces: Array; + marketplaceLoadErrors: Array; + featuredPluginIds: Array; +}; diff --git a/src/runtimes/openai/generated/v2/PluginMarketplaceEntry.ts b/src/runtimes/openai/generated/v2/PluginMarketplaceEntry.ts new file mode 100644 index 0000000..d832799 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginMarketplaceEntry.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { MarketplaceInterface } from "./MarketplaceInterface"; +import type { PluginSummary } from "./PluginSummary"; + +export type PluginMarketplaceEntry = { + name: string; + /** + * Local marketplace file path when the marketplace is backed by a local file. + * Remote-only catalog marketplaces do not have a local path. + */ + path: AbsolutePathBuf | null; + interface: MarketplaceInterface | null; + plugins: Array; +}; diff --git a/src/runtimes/openai/generated/v2/PluginReadParams.ts b/src/runtimes/openai/generated/v2/PluginReadParams.ts new file mode 100644 index 0000000..ac24193 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginReadParams.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type PluginReadParams = { + marketplacePath?: AbsolutePathBuf | null; + remoteMarketplaceName?: string | null; + pluginName: string; +}; diff --git a/src/runtimes/openai/generated/v2/PluginReadResponse.ts b/src/runtimes/openai/generated/v2/PluginReadResponse.ts new file mode 100644 index 0000000..7ef172c --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginReadResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginDetail } from "./PluginDetail"; + +export type PluginReadResponse = { plugin: PluginDetail }; diff --git a/src/runtimes/openai/generated/v2/PluginSearchParams.ts b/src/runtimes/openai/generated/v2/PluginSearchParams.ts new file mode 100644 index 0000000..f9ce33e --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSearchParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { PluginSearchScope } from "./PluginSearchScope"; + +export type PluginSearchParams = { + searchTerm: string; + scope?: PluginSearchScope | null; + cwds?: Array | null; + cursor?: string | null; + limit?: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/PluginSearchResponse.ts b/src/runtimes/openai/generated/v2/PluginSearchResponse.ts new file mode 100644 index 0000000..363a570 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSearchResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginSearchResult } from "./PluginSearchResult"; + +export type PluginSearchResponse = { data: Array; nextCursor: string | null }; diff --git a/src/runtimes/openai/generated/v2/PluginSearchResult.ts b/src/runtimes/openai/generated/v2/PluginSearchResult.ts new file mode 100644 index 0000000..6ab18b0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSearchResult.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { PluginSummary } from "./PluginSummary"; + +export type PluginSearchResult = { + plugin: PluginSummary; + marketplaceName: string; + marketplacePath: AbsolutePathBuf | null; +}; diff --git a/src/runtimes/openai/generated/v2/PluginSearchScope.ts b/src/runtimes/openai/generated/v2/PluginSearchScope.ts new file mode 100644 index 0000000..0def10b --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSearchScope.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginSearchScope = "global" | "workspace" | "personal"; diff --git a/src/runtimes/openai/generated/v2/PluginShareCheckoutParams.ts b/src/runtimes/openai/generated/v2/PluginShareCheckoutParams.ts new file mode 100644 index 0000000..49e20aa --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareCheckoutParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginShareCheckoutParams = { remotePluginId: string }; diff --git a/src/runtimes/openai/generated/v2/PluginShareCheckoutResponse.ts b/src/runtimes/openai/generated/v2/PluginShareCheckoutResponse.ts new file mode 100644 index 0000000..d320bb6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareCheckoutResponse.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type PluginShareCheckoutResponse = { + remotePluginId: string; + pluginId: string; + pluginName: string; + pluginPath: AbsolutePathBuf; + marketplaceName: string; + marketplacePath: AbsolutePathBuf; + remoteVersion: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/PluginShareContext.ts b/src/runtimes/openai/generated/v2/PluginShareContext.ts new file mode 100644 index 0000000..47daff1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareContext.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginShareDiscoverability } from "./PluginShareDiscoverability"; +import type { PluginSharePrincipal } from "./PluginSharePrincipal"; + +export type PluginShareContext = { + remotePluginId: string; + /** + * Version of the remote shared plugin release when available. + */ + remoteVersion: string | null; + discoverability: PluginShareDiscoverability | null; + shareUrl: string | null; + creatorAccountUserId: string | null; + creatorName: string | null; + sharePrincipals: Array | null; + canPublishToWorkspace: boolean | null; +}; diff --git a/src/runtimes/openai/generated/v2/PluginShareDeleteParams.ts b/src/runtimes/openai/generated/v2/PluginShareDeleteParams.ts new file mode 100644 index 0000000..092ac6c --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareDeleteParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginShareDeleteParams = { remotePluginId: string }; diff --git a/src/runtimes/openai/generated/v2/PluginShareDeleteResponse.ts b/src/runtimes/openai/generated/v2/PluginShareDeleteResponse.ts new file mode 100644 index 0000000..2310268 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareDeleteResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginShareDeleteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/PluginShareDiscoverability.ts b/src/runtimes/openai/generated/v2/PluginShareDiscoverability.ts new file mode 100644 index 0000000..8c22421 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareDiscoverability.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginShareDiscoverability = "LISTED" | "UNLISTED" | "PRIVATE"; diff --git a/src/runtimes/openai/generated/v2/PluginShareListItem.ts b/src/runtimes/openai/generated/v2/PluginShareListItem.ts new file mode 100644 index 0000000..3399548 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareListItem.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { PluginSummary } from "./PluginSummary"; + +export type PluginShareListItem = { + plugin: PluginSummary; + localPluginPath: AbsolutePathBuf | null; +}; diff --git a/src/runtimes/openai/generated/v2/PluginShareListParams.ts b/src/runtimes/openai/generated/v2/PluginShareListParams.ts new file mode 100644 index 0000000..167ace7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareListParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginShareListParams = Record; diff --git a/src/runtimes/openai/generated/v2/PluginShareListResponse.ts b/src/runtimes/openai/generated/v2/PluginShareListResponse.ts new file mode 100644 index 0000000..998ec88 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareListResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginShareListItem } from "./PluginShareListItem"; + +export type PluginShareListResponse = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/PluginSharePrincipal.ts b/src/runtimes/openai/generated/v2/PluginSharePrincipal.ts new file mode 100644 index 0000000..2014a36 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSharePrincipal.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginSharePrincipalRole } from "./PluginSharePrincipalRole"; +import type { PluginSharePrincipalType } from "./PluginSharePrincipalType"; + +export type PluginSharePrincipal = { + principalType: PluginSharePrincipalType; + principalId: string; + role: PluginSharePrincipalRole; + name: string; +}; diff --git a/src/runtimes/openai/generated/v2/PluginSharePrincipalRole.ts b/src/runtimes/openai/generated/v2/PluginSharePrincipalRole.ts new file mode 100644 index 0000000..0a022a0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSharePrincipalRole.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginSharePrincipalRole = "reader" | "editor" | "owner"; diff --git a/src/runtimes/openai/generated/v2/PluginSharePrincipalType.ts b/src/runtimes/openai/generated/v2/PluginSharePrincipalType.ts new file mode 100644 index 0000000..e54c129 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSharePrincipalType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginSharePrincipalType = "user" | "group" | "workspace"; diff --git a/src/runtimes/openai/generated/v2/PluginShareSaveParams.ts b/src/runtimes/openai/generated/v2/PluginShareSaveParams.ts new file mode 100644 index 0000000..c2a5451 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareSaveParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { PluginShareDiscoverability } from "./PluginShareDiscoverability"; +import type { PluginShareTarget } from "./PluginShareTarget"; + +export type PluginShareSaveParams = { + pluginPath: AbsolutePathBuf; + remotePluginId?: string | null; + discoverability?: PluginShareDiscoverability | null; + shareTargets?: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/PluginShareSaveResponse.ts b/src/runtimes/openai/generated/v2/PluginShareSaveResponse.ts new file mode 100644 index 0000000..a97f450 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareSaveResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginShareSaveResponse = { + remotePluginId: string; + shareUrl: string; + canPublishToWorkspace: boolean | null; +}; diff --git a/src/runtimes/openai/generated/v2/PluginShareTarget.ts b/src/runtimes/openai/generated/v2/PluginShareTarget.ts new file mode 100644 index 0000000..945607c --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareTarget.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginSharePrincipalType } from "./PluginSharePrincipalType"; +import type { PluginShareTargetRole } from "./PluginShareTargetRole"; + +export type PluginShareTarget = { + principalType: PluginSharePrincipalType; + principalId: string; + role: PluginShareTargetRole; +}; diff --git a/src/runtimes/openai/generated/v2/PluginShareTargetRole.ts b/src/runtimes/openai/generated/v2/PluginShareTargetRole.ts new file mode 100644 index 0000000..95eee17 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareTargetRole.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginShareTargetRole = "reader" | "editor"; diff --git a/src/runtimes/openai/generated/v2/PluginShareUpdateDiscoverability.ts b/src/runtimes/openai/generated/v2/PluginShareUpdateDiscoverability.ts new file mode 100644 index 0000000..767acae --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareUpdateDiscoverability.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginShareUpdateDiscoverability = "UNLISTED" | "PRIVATE" | "LISTED"; diff --git a/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsParams.ts b/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsParams.ts new file mode 100644 index 0000000..9d7ba4a --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsParams.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginShareTarget } from "./PluginShareTarget"; +import type { PluginShareUpdateDiscoverability } from "./PluginShareUpdateDiscoverability"; + +export type PluginShareUpdateTargetsParams = { + remotePluginId: string; + discoverability: PluginShareUpdateDiscoverability; + shareTargets: Array; +}; diff --git a/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsResponse.ts b/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsResponse.ts new file mode 100644 index 0000000..ba991f5 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginShareDiscoverability } from "./PluginShareDiscoverability"; +import type { PluginSharePrincipal } from "./PluginSharePrincipal"; + +export type PluginShareUpdateTargetsResponse = { + principals: Array; + discoverability: PluginShareDiscoverability; +}; diff --git a/src/runtimes/openai/generated/v2/PluginSkillReadParams.ts b/src/runtimes/openai/generated/v2/PluginSkillReadParams.ts new file mode 100644 index 0000000..5145b2c --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSkillReadParams.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginSkillReadParams = { + remoteMarketplaceName: string; + remotePluginId: string; + skillName: string; +}; diff --git a/src/runtimes/openai/generated/v2/PluginSkillReadResponse.ts b/src/runtimes/openai/generated/v2/PluginSkillReadResponse.ts new file mode 100644 index 0000000..98007bd --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSkillReadResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginSkillReadResponse = { contents: string | null }; diff --git a/src/runtimes/openai/generated/v2/PluginSource.ts b/src/runtimes/openai/generated/v2/PluginSource.ts new file mode 100644 index 0000000..cf2eb27 --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSource.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type PluginSource = + | { type: "local"; path: AbsolutePathBuf } + | { type: "git"; url: string; path: string | null; refName: string | null; sha: string | null } + | { + type: "npm"; + package: string; + /** + * Optional npm version or version range. + */ + version: string | null; + /** + * Optional HTTPS registry URL. Authentication stays in the user's npm config. + */ + registry: string | null; + } + | { type: "remote" }; diff --git a/src/runtimes/openai/generated/v2/PluginSummary.ts b/src/runtimes/openai/generated/v2/PluginSummary.ts new file mode 100644 index 0000000..5a7552c --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginSummary.ts @@ -0,0 +1,57 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginAuthPolicy } from "./PluginAuthPolicy"; +import type { PluginAvailability } from "./PluginAvailability"; +import type { PluginDisabledReason } from "./PluginDisabledReason"; +import type { PluginInstallPolicy } from "./PluginInstallPolicy"; +import type { PluginInstallPolicySource } from "./PluginInstallPolicySource"; +import type { PluginInterface } from "./PluginInterface"; +import type { PluginShareContext } from "./PluginShareContext"; +import type { PluginSource } from "./PluginSource"; + +export type PluginSummary = { + id: string; + /** + * Backend remote plugin identifier when available. + */ + remotePluginId: string | null; + /** + * Version advertised by the remote marketplace backend when available. + */ + version: string | null; + /** + * Version of the locally materialized plugin package when available. + */ + localVersion: string | null; + name: string; + /** + * Remote sharing context associated with this plugin when available. + */ + shareContext: PluginShareContext | null; + source: PluginSource; + installed: boolean; + /** + * Unix timestamp in seconds when the remote plugin was installed, when available. + */ + installedAt: number | null; + enabled: boolean; + installPolicy: PluginInstallPolicy; + installPolicySource: PluginInstallPolicySource | null; + mustShowInstallationInterstitial: boolean | null; + authPolicy: PluginAuthPolicy; + /** + * Availability state for installing and using the plugin. + */ + availability: PluginAvailability; + /** + * Why the remote plugin is unavailable, when provided by plugin-service. + */ + disabledReason: PluginDisabledReason | null; + /** + * Raw plugin-service plan identifiers eligible to install the plugin. + */ + eligiblePlanTypes: Array | null; + interface: PluginInterface | null; + keywords: Array; +}; diff --git a/src/runtimes/openai/generated/v2/PluginUninstallParams.ts b/src/runtimes/openai/generated/v2/PluginUninstallParams.ts new file mode 100644 index 0000000..250ee7d --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginUninstallParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginUninstallParams = { pluginId: string }; diff --git a/src/runtimes/openai/generated/v2/PluginUninstallResponse.ts b/src/runtimes/openai/generated/v2/PluginUninstallResponse.ts new file mode 100644 index 0000000..5d02c2f --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginUninstallResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginUninstallResponse = Record; diff --git a/src/runtimes/openai/generated/v2/PluginsMigration.ts b/src/runtimes/openai/generated/v2/PluginsMigration.ts new file mode 100644 index 0000000..3d882cf --- /dev/null +++ b/src/runtimes/openai/generated/v2/PluginsMigration.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginsMigration = { marketplaceName: string; pluginNames: Array }; diff --git a/src/runtimes/openai/generated/v2/ProcessExitedNotification.ts b/src/runtimes/openai/generated/v2/ProcessExitedNotification.ts new file mode 100644 index 0000000..16d4952 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessExitedNotification.ts @@ -0,0 +1,43 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Final process exit notification for `process/spawn`. + */ +export type ProcessExitedNotification = { + /** + * Client-supplied, connection-scoped `processHandle` from `process/spawn`. + */ + processHandle: string; + /** + * Process exit code. + */ + exitCode: number; + /** + * Buffered stdout capture. + * + * Empty when stdout was streamed via `process/outputDelta`. + */ + stdout: string; + /** + * Whether stdout reached `outputBytesCap`. + * + * In streaming mode, stdout is empty and cap state is also reported on the + * final stdout `process/outputDelta` notification. + */ + stdoutCapReached: boolean; + /** + * Buffered stderr capture. + * + * Empty when stderr was streamed via `process/outputDelta`. + */ + stderr: string; + /** + * Whether stderr reached `outputBytesCap`. + * + * In streaming mode, stderr is empty and cap state is also reported on the + * final stderr `process/outputDelta` notification. + */ + stderrCapReached: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ProcessKillParams.ts b/src/runtimes/openai/generated/v2/ProcessKillParams.ts new file mode 100644 index 0000000..51e9603 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessKillParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Terminate a running `process/spawn` session. + */ +export type ProcessKillParams = { + /** + * Client-supplied, connection-scoped `processHandle` from `process/spawn`. + */ + processHandle: string; +}; diff --git a/src/runtimes/openai/generated/v2/ProcessKillResponse.ts b/src/runtimes/openai/generated/v2/ProcessKillResponse.ts new file mode 100644 index 0000000..d1bd824 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessKillResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Empty success response for `process/kill`. + */ +export type ProcessKillResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProcessOutputDeltaNotification.ts b/src/runtimes/openai/generated/v2/ProcessOutputDeltaNotification.ts new file mode 100644 index 0000000..eeaeec7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessOutputDeltaNotification.ts @@ -0,0 +1,27 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProcessOutputStream } from "./ProcessOutputStream"; + +/** + * Base64-encoded output chunk emitted for a streaming `process/spawn` request. + */ +export type ProcessOutputDeltaNotification = { + /** + * Client-supplied, connection-scoped `processHandle` from `process/spawn`. + */ + processHandle: string; + /** + * Output stream this chunk belongs to. + */ + stream: ProcessOutputStream; + /** + * Base64-encoded output bytes. + */ + deltaBase64: string; + /** + * True on the final streamed chunk for this stream when output was + * truncated by `outputBytesCap`. + */ + capReached: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ProcessOutputStream.ts b/src/runtimes/openai/generated/v2/ProcessOutputStream.ts new file mode 100644 index 0000000..1bb550d --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessOutputStream.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Stream label for `process/outputDelta` notifications. + */ +export type ProcessOutputStream = "stdout" | "stderr"; diff --git a/src/runtimes/openai/generated/v2/ProcessResizePtyParams.ts b/src/runtimes/openai/generated/v2/ProcessResizePtyParams.ts new file mode 100644 index 0000000..da4cbe4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessResizePtyParams.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProcessTerminalSize } from "./ProcessTerminalSize"; + +/** + * Resize a running PTY-backed `process/spawn` session. + */ +export type ProcessResizePtyParams = { + /** + * Client-supplied, connection-scoped `processHandle` from `process/spawn`. + */ + processHandle: string; + /** + * New PTY size in character cells. + */ + size: ProcessTerminalSize; +}; diff --git a/src/runtimes/openai/generated/v2/ProcessResizePtyResponse.ts b/src/runtimes/openai/generated/v2/ProcessResizePtyResponse.ts new file mode 100644 index 0000000..5d06355 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessResizePtyResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Empty success response for `process/resizePty`. + */ +export type ProcessResizePtyResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProcessSpawnParams.ts b/src/runtimes/openai/generated/v2/ProcessSpawnParams.ts new file mode 100644 index 0000000..9202b60 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessSpawnParams.ts @@ -0,0 +1,74 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { ProcessTerminalSize } from "./ProcessTerminalSize"; + +/** + * Spawn a standalone process (argv vector) without a Codex sandbox on the host + * where the app server is running. + * + * `process/spawn` returns after the process has started and the connection-scoped + * `processHandle` has been registered. Process output and exit are reported via + * `process/outputDelta` and `process/exited` notifications. + */ +export type ProcessSpawnParams = { + /** + * Command argv vector. Empty arrays are rejected. + */ + command: Array; + /** + * Client-supplied, connection-scoped process handle. + * + * Duplicate active handles are rejected on the same connection. The same + * handle can be reused after the prior process exits. + */ + processHandle: string; + /** + * Absolute working directory for the process. + */ + cwd: AbsolutePathBuf; + /** + * Enable PTY mode. + * + * This implies `streamStdin` and `streamStdoutStderr`. + */ + tty?: boolean; + /** + * Allow follow-up `process/writeStdin` requests to write stdin bytes. + */ + streamStdin?: boolean; + /** + * Stream stdout/stderr via `process/outputDelta` notifications. + * + * Streamed bytes are not duplicated into the `process/exited` notification. + */ + streamStdoutStderr?: boolean; + /** + * Optional per-stream stdout/stderr capture cap in bytes. + * + * When omitted, the server default applies. Set to `null` to disable the + * cap. + */ + outputBytesCap?: number | null; + /** + * Optional timeout in milliseconds. + * + * When omitted, the server default applies. Set to `null` to disable the + * timeout. + */ + timeoutMs?: number | null; + /** + * Optional environment overrides merged into the app-server process + * environment. + * + * Matching names override inherited values. Set a key to `null` to unset + * an inherited variable. + */ + env?: { [key in string]?: string | null } | null; + /** + * Optional initial PTY size in character cells. Only valid when `tty` is + * true. + */ + size?: ProcessTerminalSize | null; +}; diff --git a/src/runtimes/openai/generated/v2/ProcessSpawnResponse.ts b/src/runtimes/openai/generated/v2/ProcessSpawnResponse.ts new file mode 100644 index 0000000..57b5222 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessSpawnResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `process/spawn`. + */ +export type ProcessSpawnResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProcessTerminalSize.ts b/src/runtimes/openai/generated/v2/ProcessTerminalSize.ts new file mode 100644 index 0000000..3f93ee6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessTerminalSize.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * PTY size in character cells for `process/spawn` PTY sessions. + */ +export type ProcessTerminalSize = { + /** + * Terminal height in character cells. + */ + rows: number; + /** + * Terminal width in character cells. + */ + cols: number; +}; diff --git a/src/runtimes/openai/generated/v2/ProcessWriteStdinParams.ts b/src/runtimes/openai/generated/v2/ProcessWriteStdinParams.ts new file mode 100644 index 0000000..853145f --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessWriteStdinParams.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Write stdin bytes to a running `process/spawn` session, close stdin, or + * both. + */ +export type ProcessWriteStdinParams = { + /** + * Client-supplied, connection-scoped `processHandle` from `process/spawn`. + */ + processHandle: string; + /** + * Optional base64-encoded stdin bytes to write. + */ + deltaBase64?: string | null; + /** + * Close stdin after writing `deltaBase64`, if present. + */ + closeStdin?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ProcessWriteStdinResponse.ts b/src/runtimes/openai/generated/v2/ProcessWriteStdinResponse.ts new file mode 100644 index 0000000..29ba811 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProcessWriteStdinResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Empty success response for `process/writeStdin`. + */ +export type ProcessWriteStdinResponse = Record; diff --git a/src/runtimes/openai/generated/v2/Project.ts b/src/runtimes/openai/generated/v2/Project.ts new file mode 100644 index 0000000..7d5cebb --- /dev/null +++ b/src/runtimes/openai/generated/v2/Project.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProjectRoot } from "./ProjectRoot"; + +export type Project = { + id: string; + name: string; + roots: Array; + metadata: { [key in string]?: string }; + position: number; + createdAt: number; + updatedAt: number; +}; diff --git a/src/runtimes/openai/generated/v2/ProjectChangeType.ts b/src/runtimes/openai/generated/v2/ProjectChangeType.ts new file mode 100644 index 0000000..7c589c0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectChangeType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProjectChangeType = "created" | "updated" | "deleted"; diff --git a/src/runtimes/openai/generated/v2/ProjectChangedNotification.ts b/src/runtimes/openai/generated/v2/ProjectChangedNotification.ts new file mode 100644 index 0000000..ce58167 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectChangedNotification.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProjectChangeType } from "./ProjectChangeType"; + +export type ProjectChangedNotification = { projectId: string; changeType: ProjectChangeType }; diff --git a/src/runtimes/openai/generated/v2/ProjectCreateParams.ts b/src/runtimes/openai/generated/v2/ProjectCreateParams.ts new file mode 100644 index 0000000..f2760db --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectCreateParams.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProjectRoot } from "./ProjectRoot"; + +export type ProjectCreateParams = { + name: string; + roots: Array; + metadata?: { [key in string]?: string } | null; + idempotencyKey: string; +}; diff --git a/src/runtimes/openai/generated/v2/ProjectCreateResponse.ts b/src/runtimes/openai/generated/v2/ProjectCreateResponse.ts new file mode 100644 index 0000000..d22e491 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectCreateResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Project } from "./Project"; + +export type ProjectCreateResponse = { project: Project }; diff --git a/src/runtimes/openai/generated/v2/ProjectDeleteParams.ts b/src/runtimes/openai/generated/v2/ProjectDeleteParams.ts new file mode 100644 index 0000000..5a079c9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectDeleteParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProjectDeleteParams = { projectId: string }; diff --git a/src/runtimes/openai/generated/v2/ProjectDeleteResponse.ts b/src/runtimes/openai/generated/v2/ProjectDeleteResponse.ts new file mode 100644 index 0000000..772fd2d --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectDeleteResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProjectDeleteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProjectImportParams.ts b/src/runtimes/openai/generated/v2/ProjectImportParams.ts new file mode 100644 index 0000000..c40226c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectImportParams.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProjectRoot } from "./ProjectRoot"; + +export type ProjectImportParams = { + name: string; + roots: Array; + metadata?: { [key in string]?: string } | null; + threads?: Array | null; + idempotencyKey: string; +}; diff --git a/src/runtimes/openai/generated/v2/ProjectImportResponse.ts b/src/runtimes/openai/generated/v2/ProjectImportResponse.ts new file mode 100644 index 0000000..90d050c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectImportResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Project } from "./Project"; + +export type ProjectImportResponse = { project: Project }; diff --git a/src/runtimes/openai/generated/v2/ProjectListParams.ts b/src/runtimes/openai/generated/v2/ProjectListParams.ts new file mode 100644 index 0000000..ff3c49c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectListParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProjectListParams = { cursor?: string | null; limit?: number | null }; diff --git a/src/runtimes/openai/generated/v2/ProjectListResponse.ts b/src/runtimes/openai/generated/v2/ProjectListResponse.ts new file mode 100644 index 0000000..1fbd21e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectListResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Project } from "./Project"; + +export type ProjectListResponse = { data: Array; nextCursor: string | null }; diff --git a/src/runtimes/openai/generated/v2/ProjectMoveParams.ts b/src/runtimes/openai/generated/v2/ProjectMoveParams.ts new file mode 100644 index 0000000..4393826 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectMoveParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProjectMoveParams = { projectId: string; beforeProjectId?: string | null }; diff --git a/src/runtimes/openai/generated/v2/ProjectMoveResponse.ts b/src/runtimes/openai/generated/v2/ProjectMoveResponse.ts new file mode 100644 index 0000000..38a1485 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectMoveResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProjectMoveResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProjectReadParams.ts b/src/runtimes/openai/generated/v2/ProjectReadParams.ts new file mode 100644 index 0000000..90ffa2b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectReadParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProjectReadParams = { projectId: string }; diff --git a/src/runtimes/openai/generated/v2/ProjectReadResponse.ts b/src/runtimes/openai/generated/v2/ProjectReadResponse.ts new file mode 100644 index 0000000..1cb85a8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectReadResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Project } from "./Project"; + +export type ProjectReadResponse = { project: Project }; diff --git a/src/runtimes/openai/generated/v2/ProjectRoot.ts b/src/runtimes/openai/generated/v2/ProjectRoot.ts new file mode 100644 index 0000000..8be788b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectRoot.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type ProjectRoot = { path: AbsolutePathBuf }; diff --git a/src/runtimes/openai/generated/v2/ProjectUpdateParams.ts b/src/runtimes/openai/generated/v2/ProjectUpdateParams.ts new file mode 100644 index 0000000..062fec1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectUpdateParams.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProjectRoot } from "./ProjectRoot"; + +export type ProjectUpdateParams = { + projectId: string; + name?: string | null; + roots?: Array | null; + metadata?: { [key in string]?: string } | null; +}; diff --git a/src/runtimes/openai/generated/v2/ProjectUpdateResponse.ts b/src/runtimes/openai/generated/v2/ProjectUpdateResponse.ts new file mode 100644 index 0000000..8d42e97 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ProjectUpdateResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Project } from "./Project"; + +export type ProjectUpdateResponse = { project: Project }; diff --git a/src/runtimes/openai/generated/v2/QueuedSubmission.ts b/src/runtimes/openai/generated/v2/QueuedSubmission.ts new file mode 100644 index 0000000..80b300c --- /dev/null +++ b/src/runtimes/openai/generated/v2/QueuedSubmission.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { UserInput } from "./UserInput"; + +export type QueuedSubmission = { id: string; input: Array; clientUserMessageId: string }; diff --git a/src/runtimes/openai/generated/v2/RateLimitReachedType.ts b/src/runtimes/openai/generated/v2/RateLimitReachedType.ts new file mode 100644 index 0000000..84bb0b5 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RateLimitReachedType.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RateLimitReachedType = + | "rate_limit_reached" + | "workspace_owner_credits_depleted" + | "workspace_member_credits_depleted" + | "workspace_owner_usage_limit_reached" + | "workspace_member_usage_limit_reached"; diff --git a/src/runtimes/openai/generated/v2/RateLimitResetCredit.ts b/src/runtimes/openai/generated/v2/RateLimitResetCredit.ts new file mode 100644 index 0000000..fc8ad41 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RateLimitResetCredit.ts @@ -0,0 +1,30 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus"; +import type { RateLimitResetType } from "./RateLimitResetType"; + +export type RateLimitResetCredit = { + /** + * Opaque backend identifier for this reset credit. + */ + id: string; + resetType: RateLimitResetType; + status: RateLimitResetCreditStatus; + /** + * Unix timestamp in seconds when the credit was granted. + */ + grantedAt: number; + /** + * Unix timestamp in seconds when the credit expires, or `null` if it does not expire. + */ + expiresAt: number | null; + /** + * Backend-provided display title for this credit, or `null` when unavailable. + */ + title: string | null; + /** + * Backend-provided display description for this credit, or `null` when unavailable. + */ + description: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/RateLimitResetCreditStatus.ts b/src/runtimes/openai/generated/v2/RateLimitResetCreditStatus.ts new file mode 100644 index 0000000..fa15861 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RateLimitResetCreditStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RateLimitResetCreditStatus = "available" | "redeeming" | "redeemed" | "unknown"; diff --git a/src/runtimes/openai/generated/v2/RateLimitResetCreditsSummary.ts b/src/runtimes/openai/generated/v2/RateLimitResetCreditsSummary.ts new file mode 100644 index 0000000..4fb121c --- /dev/null +++ b/src/runtimes/openai/generated/v2/RateLimitResetCreditsSummary.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimitResetCredit } from "./RateLimitResetCredit"; + +export type RateLimitResetCreditsSummary = { + availableCount: bigint; + /** + * Detail rows for available reset credits, when the backend provides them. + * + * `null` means only `availableCount` is known, while an empty array means details were fetched + * and no available credits were returned. The backend may cap this list, so its length can be + * less than `availableCount`. + */ + credits: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/RateLimitResetType.ts b/src/runtimes/openai/generated/v2/RateLimitResetType.ts new file mode 100644 index 0000000..718145b --- /dev/null +++ b/src/runtimes/openai/generated/v2/RateLimitResetType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RateLimitResetType = "codexRateLimits" | "unknown"; diff --git a/src/runtimes/openai/generated/v2/RateLimitSnapshot.ts b/src/runtimes/openai/generated/v2/RateLimitSnapshot.ts new file mode 100644 index 0000000..29676de --- /dev/null +++ b/src/runtimes/openai/generated/v2/RateLimitSnapshot.ts @@ -0,0 +1,23 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PlanType } from "../PlanType"; +import type { CreditsSnapshot } from "./CreditsSnapshot"; +import type { RateLimitReachedType } from "./RateLimitReachedType"; +import type { RateLimitWindow } from "./RateLimitWindow"; +import type { SpendControlLimitSnapshot } from "./SpendControlLimitSnapshot"; + +export type RateLimitSnapshot = { + limitId: string | null; + limitName: string | null; + primary: RateLimitWindow | null; + secondary: RateLimitWindow | null; + credits: CreditsSnapshot | null; + individualLimit: SpendControlLimitSnapshot | null; + /** + * Backend-reported spend-control state. `None` is unavailable, not a sparse-update recovery. + */ + spendControlReached: boolean | null; + planType: PlanType | null; + rateLimitReachedType: RateLimitReachedType | null; +}; diff --git a/src/runtimes/openai/generated/v2/RateLimitWindow.ts b/src/runtimes/openai/generated/v2/RateLimitWindow.ts new file mode 100644 index 0000000..c0ad595 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RateLimitWindow.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RateLimitWindow = { + usedPercent: number; + windowDurationMins: number | null; + resetsAt: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/RawResponseCompletedNotification.ts b/src/runtimes/openai/generated/v2/RawResponseCompletedNotification.ts new file mode 100644 index 0000000..ccf5169 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RawResponseCompletedNotification.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; + +/** + * Internal-only notification containing the exact usage from one upstream + * Responses API completion. + */ +export type RawResponseCompletedNotification = { + threadId: string; + turnId: string; + responseId: string; + usage: TokenUsageBreakdown | null; +}; diff --git a/src/runtimes/openai/generated/v2/RawResponseItemCompletedNotification.ts b/src/runtimes/openai/generated/v2/RawResponseItemCompletedNotification.ts new file mode 100644 index 0000000..b0458cd --- /dev/null +++ b/src/runtimes/openai/generated/v2/RawResponseItemCompletedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ResponseItem } from "../ResponseItem"; + +export type RawResponseItemCompletedNotification = { + threadId: string; + turnId: string; + item: ResponseItem; +}; diff --git a/src/runtimes/openai/generated/v2/ReasoningEffortOption.ts b/src/runtimes/openai/generated/v2/ReasoningEffortOption.ts new file mode 100644 index 0000000..bc26f95 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ReasoningEffortOption.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReasoningEffort } from "../ReasoningEffort"; + +export type ReasoningEffortOption = { reasoningEffort: ReasoningEffort; description: string }; diff --git a/src/runtimes/openai/generated/v2/ReasoningSummaryPartAddedNotification.ts b/src/runtimes/openai/generated/v2/ReasoningSummaryPartAddedNotification.ts new file mode 100644 index 0000000..3ba8282 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ReasoningSummaryPartAddedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReasoningSummaryPartAddedNotification = { + threadId: string; + turnId: string; + itemId: string; + summaryIndex: number; +}; diff --git a/src/runtimes/openai/generated/v2/ReasoningSummaryTextDeltaNotification.ts b/src/runtimes/openai/generated/v2/ReasoningSummaryTextDeltaNotification.ts new file mode 100644 index 0000000..7ce5951 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ReasoningSummaryTextDeltaNotification.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReasoningSummaryTextDeltaNotification = { + threadId: string; + turnId: string; + itemId: string; + delta: string; + summaryIndex: number; +}; diff --git a/src/runtimes/openai/generated/v2/ReasoningTextDeltaNotification.ts b/src/runtimes/openai/generated/v2/ReasoningTextDeltaNotification.ts new file mode 100644 index 0000000..23e9bc4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ReasoningTextDeltaNotification.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReasoningTextDeltaNotification = { + threadId: string; + turnId: string; + itemId: string; + delta: string; + contentIndex: number; +}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClient.ts b/src/runtimes/openai/generated/v2/RemoteControlClient.ts new file mode 100644 index 0000000..c66242d --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlClient.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlClient = { + clientId: string; + displayName: string | null; + deviceType: string | null; + platform: string | null; + osVersion: string | null; + deviceModel: string | null; + appVersion: string | null; + lastSeenAt: bigint | null; +}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsListOrder.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsListOrder.ts new file mode 100644 index 0000000..7166235 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlClientsListOrder.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlClientsListOrder = "asc" | "desc"; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsListParams.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsListParams.ts new file mode 100644 index 0000000..ba6468a --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlClientsListParams.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RemoteControlClientsListOrder } from "./RemoteControlClientsListOrder"; + +export type RemoteControlClientsListParams = { + environmentId: string; + cursor?: string | null; + limit?: number | null; + order?: RemoteControlClientsListOrder | null; +}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsListResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsListResponse.ts new file mode 100644 index 0000000..c7261f6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlClientsListResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RemoteControlClient } from "./RemoteControlClient"; + +export type RemoteControlClientsListResponse = { + data: Array; + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeParams.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeParams.ts new file mode 100644 index 0000000..f3b93a8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlClientsRevokeParams = { environmentId: string; clientId: string }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeResponse.ts new file mode 100644 index 0000000..d30e90c --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlClientsRevokeResponse = Record; diff --git a/src/runtimes/openai/generated/v2/RemoteControlConnectionStatus.ts b/src/runtimes/openai/generated/v2/RemoteControlConnectionStatus.ts new file mode 100644 index 0000000..3e6197f --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlConnectionStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlConnectionStatus = "disabled" | "connecting" | "connected" | "errored"; diff --git a/src/runtimes/openai/generated/v2/RemoteControlDisableParams.ts b/src/runtimes/openai/generated/v2/RemoteControlDisableParams.ts new file mode 100644 index 0000000..ec9af47 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlDisableParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlDisableParams = { ephemeral?: boolean }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlDisableResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlDisableResponse.ts new file mode 100644 index 0000000..023ee48 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlDisableResponse.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; + +export type RemoteControlDisableResponse = { + status: RemoteControlConnectionStatus; + serverName: string; + installationId: string; + environmentId: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlEnableParams.ts b/src/runtimes/openai/generated/v2/RemoteControlEnableParams.ts new file mode 100644 index 0000000..e724071 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlEnableParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlEnableParams = { ephemeral?: boolean }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlEnableResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlEnableResponse.ts new file mode 100644 index 0000000..c4199f4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlEnableResponse.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; + +export type RemoteControlEnableResponse = { + status: RemoteControlConnectionStatus; + serverName: string; + installationId: string; + environmentId: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlPairingStartParams.ts b/src/runtimes/openai/generated/v2/RemoteControlPairingStartParams.ts new file mode 100644 index 0000000..444bc58 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlPairingStartParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlPairingStartParams = { manualCode?: boolean }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlPairingStartResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlPairingStartResponse.ts new file mode 100644 index 0000000..08b86ad --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlPairingStartResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlPairingStartResponse = { + pairingCode: string; + manualPairingCode: string | null; + environmentId: string; + expiresAt: bigint; +}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlPairingStatusParams.ts b/src/runtimes/openai/generated/v2/RemoteControlPairingStatusParams.ts new file mode 100644 index 0000000..3e0536a --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlPairingStatusParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlPairingStatusParams = { + pairingCode?: string | null; + manualPairingCode?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlPairingStatusResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlPairingStatusResponse.ts new file mode 100644 index 0000000..92b301f --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlPairingStatusResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoteControlPairingStatusResponse = { claimed: boolean }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlStatusChangedNotification.ts b/src/runtimes/openai/generated/v2/RemoteControlStatusChangedNotification.ts new file mode 100644 index 0000000..58b0350 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlStatusChangedNotification.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; + +/** + * Current remote-control connection status and remote identity exposed to clients. + */ +export type RemoteControlStatusChangedNotification = { + status: RemoteControlConnectionStatus; + serverName: string; + installationId: string; + environmentId: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlStatusReadResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlStatusReadResponse.ts new file mode 100644 index 0000000..2421b29 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RemoteControlStatusReadResponse.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; + +export type RemoteControlStatusReadResponse = { + status: RemoteControlConnectionStatus; + serverName: string; + installationId: string; + environmentId: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/RequestPermissionProfile.ts b/src/runtimes/openai/generated/v2/RequestPermissionProfile.ts new file mode 100644 index 0000000..f8350a0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/RequestPermissionProfile.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; +import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; + +export type RequestPermissionProfile = { + network: AdditionalNetworkPermissions | null; + fileSystem: AdditionalFileSystemPermissions | null; +}; diff --git a/src/runtimes/openai/generated/v2/ResidencyRequirement.ts b/src/runtimes/openai/generated/v2/ResidencyRequirement.ts new file mode 100644 index 0000000..1699c84 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ResidencyRequirement.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ResidencyRequirement = "us"; diff --git a/src/runtimes/openai/generated/v2/ReviewDelivery.ts b/src/runtimes/openai/generated/v2/ReviewDelivery.ts new file mode 100644 index 0000000..8fbccd1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ReviewDelivery.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReviewDelivery = "inline" | "detached"; diff --git a/src/runtimes/openai/generated/v2/ReviewStartParams.ts b/src/runtimes/openai/generated/v2/ReviewStartParams.ts new file mode 100644 index 0000000..bd72fad --- /dev/null +++ b/src/runtimes/openai/generated/v2/ReviewStartParams.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewDelivery } from "./ReviewDelivery"; +import type { ReviewTarget } from "./ReviewTarget"; + +export type ReviewStartParams = { + threadId: string; + target: ReviewTarget; + /** + * Where to run the review: inline (default) on the current thread or + * detached on a new thread (returned in `reviewThreadId`). + */ + delivery?: ReviewDelivery | null; +}; diff --git a/src/runtimes/openai/generated/v2/ReviewStartResponse.ts b/src/runtimes/openai/generated/v2/ReviewStartResponse.ts new file mode 100644 index 0000000..9440202 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ReviewStartResponse.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Turn } from "./Turn"; + +export type ReviewStartResponse = { + turn: Turn; + /** + * Identifies the thread where the review runs. + * + * For inline reviews, this is the original thread id. + * For detached reviews, this is the id of the new review thread. + */ + reviewThreadId: string; +}; diff --git a/src/runtimes/openai/generated/v2/ReviewTarget.ts b/src/runtimes/openai/generated/v2/ReviewTarget.ts new file mode 100644 index 0000000..640a5d8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ReviewTarget.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReviewTarget = + | { type: "uncommittedChanges" } + | { type: "baseBranch"; branch: string } + | { + type: "commit"; + sha: string; + /** + * Optional human-readable label (e.g., commit subject) for UIs. + */ + title: string | null; + } + | { type: "custom"; instructions: string }; diff --git a/src/runtimes/openai/generated/v2/SandboxMode.ts b/src/runtimes/openai/generated/v2/SandboxMode.ts new file mode 100644 index 0000000..b8cf432 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SandboxMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access"; diff --git a/src/runtimes/openai/generated/v2/SandboxPolicy.ts b/src/runtimes/openai/generated/v2/SandboxPolicy.ts new file mode 100644 index 0000000..fe2700b --- /dev/null +++ b/src/runtimes/openai/generated/v2/SandboxPolicy.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { NetworkAccess } from "./NetworkAccess"; + +export type SandboxPolicy = + | { type: "dangerFullAccess" } + | { type: "readOnly"; networkAccess: boolean } + | { type: "externalSandbox"; networkAccess: NetworkAccess } + | { + type: "workspaceWrite"; + writableRoots: Array; + networkAccess: boolean; + excludeTmpdirEnvVar: boolean; + excludeSlashTmp: boolean; + }; diff --git a/src/runtimes/openai/generated/v2/SandboxWorkspaceWrite.ts b/src/runtimes/openai/generated/v2/SandboxWorkspaceWrite.ts new file mode 100644 index 0000000..c7e4cb6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SandboxWorkspaceWrite.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SandboxWorkspaceWrite = { + writable_roots: Array; + network_access: boolean; + exclude_tmpdir_env_var: boolean; + exclude_slash_tmp: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ScheduledTaskSchedule.ts b/src/runtimes/openai/generated/v2/ScheduledTaskSchedule.ts new file mode 100644 index 0000000..4617cf3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ScheduledTaskSchedule.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ScheduledTaskWeekday } from "./ScheduledTaskWeekday"; + +export type ScheduledTaskSchedule = + | { type: "hourly"; intervalHours: number; days: Array | null } + | { type: "daily"; time: string } + | { type: "weekdays"; time: string } + | { type: "weekly"; days: Array; time: string }; diff --git a/src/runtimes/openai/generated/v2/ScheduledTaskSummary.ts b/src/runtimes/openai/generated/v2/ScheduledTaskSummary.ts new file mode 100644 index 0000000..218ad80 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ScheduledTaskSummary.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ScheduledTaskSchedule } from "./ScheduledTaskSchedule"; + +export type ScheduledTaskSummary = { + key: string; + name: string; + prompt: string; + schedule: ScheduledTaskSchedule; +}; diff --git a/src/runtimes/openai/generated/v2/ScheduledTaskWeekday.ts b/src/runtimes/openai/generated/v2/ScheduledTaskWeekday.ts new file mode 100644 index 0000000..bf21096 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ScheduledTaskWeekday.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ScheduledTaskWeekday = "MO" | "TU" | "WE" | "TH" | "FR" | "SA" | "SU"; diff --git a/src/runtimes/openai/generated/v2/SelectedCapabilityRoot.ts b/src/runtimes/openai/generated/v2/SelectedCapabilityRoot.ts new file mode 100644 index 0000000..65b7093 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SelectedCapabilityRoot.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CapabilityRootLocation } from "./CapabilityRootLocation"; + +/** + * A user-selected root that can expose one or more runtime capabilities. + */ +export type SelectedCapabilityRoot = { + /** + * Stable identifier supplied by the capability selection platform. + */ + id: string; + /** + * Where the selected root can be resolved. + */ + location: CapabilityRootLocation; +}; diff --git a/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailParams.ts b/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailParams.ts new file mode 100644 index 0000000..f8a3ebc --- /dev/null +++ b/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType"; + +export type SendAddCreditsNudgeEmailParams = { creditType: AddCreditsNudgeCreditType }; diff --git a/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailResponse.ts b/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailResponse.ts new file mode 100644 index 0000000..a2e096e --- /dev/null +++ b/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus"; + +export type SendAddCreditsNudgeEmailResponse = { status: AddCreditsNudgeEmailStatus }; diff --git a/src/runtimes/openai/generated/v2/ServerDiagnosticsGauge.ts b/src/runtimes/openai/generated/v2/ServerDiagnosticsGauge.ts new file mode 100644 index 0000000..6ac4563 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ServerDiagnosticsGauge.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ServerDiagnosticsGauge = { name: string; value: number }; diff --git a/src/runtimes/openai/generated/v2/ServerDiagnosticsParams.ts b/src/runtimes/openai/generated/v2/ServerDiagnosticsParams.ts new file mode 100644 index 0000000..aa1d659 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ServerDiagnosticsParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ServerDiagnosticsParams = Record; diff --git a/src/runtimes/openai/generated/v2/ServerDiagnosticsProcess.ts b/src/runtimes/openai/generated/v2/ServerDiagnosticsProcess.ts new file mode 100644 index 0000000..2caa629 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ServerDiagnosticsProcess.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ServerDiagnosticsProcess = { + id: number; + residentMemoryBytes: number | null; + physicalFootprintBytes: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ServerDiagnosticsResponse.ts b/src/runtimes/openai/generated/v2/ServerDiagnosticsResponse.ts new file mode 100644 index 0000000..5e1aad6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ServerDiagnosticsResponse.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ServerDiagnosticsGauge } from "./ServerDiagnosticsGauge"; +import type { ServerDiagnosticsProcess } from "./ServerDiagnosticsProcess"; + +export type ServerDiagnosticsResponse = { + process: ServerDiagnosticsProcess; + gauges: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ServerRequestResolvedNotification.ts b/src/runtimes/openai/generated/v2/ServerRequestResolvedNotification.ts new file mode 100644 index 0000000..a8c3645 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ServerRequestResolvedNotification.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RequestId } from "../RequestId"; + +export type ServerRequestResolvedNotification = { threadId: string; requestId: RequestId }; diff --git a/src/runtimes/openai/generated/v2/SessionMigration.ts b/src/runtimes/openai/generated/v2/SessionMigration.ts new file mode 100644 index 0000000..79a08cb --- /dev/null +++ b/src/runtimes/openai/generated/v2/SessionMigration.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SessionMigration = { path: string; cwd: string; title: string | null }; diff --git a/src/runtimes/openai/generated/v2/SessionSource.ts b/src/runtimes/openai/generated/v2/SessionSource.ts new file mode 100644 index 0000000..1d68ade --- /dev/null +++ b/src/runtimes/openai/generated/v2/SessionSource.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SubAgentSource } from "../SubAgentSource"; + +export type SessionSource = + | "cli" + | "vscode" + | "exec" + | "appServer" + | { custom: string } + | { subAgent: SubAgentSource } + | "unknown"; diff --git a/src/runtimes/openai/generated/v2/SkillDependencies.ts b/src/runtimes/openai/generated/v2/SkillDependencies.ts new file mode 100644 index 0000000..6863d3e --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillDependencies.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SkillToolDependency } from "./SkillToolDependency"; + +export type SkillDependencies = { tools: Array }; diff --git a/src/runtimes/openai/generated/v2/SkillErrorInfo.ts b/src/runtimes/openai/generated/v2/SkillErrorInfo.ts new file mode 100644 index 0000000..5d7edcc --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillErrorInfo.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillErrorInfo = { path: string; message: string }; diff --git a/src/runtimes/openai/generated/v2/SkillInterface.ts b/src/runtimes/openai/generated/v2/SkillInterface.ts new file mode 100644 index 0000000..42d43ce --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillInterface.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type SkillInterface = { + displayName?: string; + shortDescription?: string; + iconSmall?: AbsolutePathBuf; + iconLarge?: AbsolutePathBuf; + /** + * Remote small icon URL from the plugin catalog. + */ + iconSmallUrl: string | null; + /** + * Remote large icon URL from the plugin catalog. + */ + iconLargeUrl: string | null; + brandColor?: string; + defaultPrompt?: string; +}; diff --git a/src/runtimes/openai/generated/v2/SkillMetadata.ts b/src/runtimes/openai/generated/v2/SkillMetadata.ts new file mode 100644 index 0000000..183a6f7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillMetadata.ts @@ -0,0 +1,25 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { SkillDependencies } from "./SkillDependencies"; +import type { SkillInterface } from "./SkillInterface"; +import type { SkillScope } from "./SkillScope"; + +export type SkillMetadata = { + name: string; + description: string; + /** + * Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description. + */ + shortDescription?: string; + interface?: SkillInterface; + dependencies?: SkillDependencies; + path: AbsolutePathBuf; + scope: SkillScope; + enabled: boolean; + /** + * Owning plugin ID, matching `PluginSummary.id`, when known. + */ + pluginId: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/SkillMigration.ts b/src/runtimes/openai/generated/v2/SkillMigration.ts new file mode 100644 index 0000000..fd41e56 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillMigration.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/SkillScope.ts b/src/runtimes/openai/generated/v2/SkillScope.ts new file mode 100644 index 0000000..997006f --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillScope.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillScope = "user" | "repo" | "system" | "admin"; diff --git a/src/runtimes/openai/generated/v2/SkillSummary.ts b/src/runtimes/openai/generated/v2/SkillSummary.ts new file mode 100644 index 0000000..29f28af --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillSummary.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { SkillInterface } from "./SkillInterface"; + +export type SkillSummary = { + name: string; + description: string; + shortDescription: string | null; + interface: SkillInterface | null; + path: AbsolutePathBuf | null; + enabled: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/SkillToolDependency.ts b/src/runtimes/openai/generated/v2/SkillToolDependency.ts new file mode 100644 index 0000000..36a30a1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillToolDependency.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillToolDependency = { + type: string; + value: string; + description?: string; + transport?: string; + command?: string; + url?: string; +}; diff --git a/src/runtimes/openai/generated/v2/SkillsChangedNotification.ts b/src/runtimes/openai/generated/v2/SkillsChangedNotification.ts new file mode 100644 index 0000000..23ed93a --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillsChangedNotification.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Notification emitted when watched local skill files change. + * + * Treat this as an invalidation signal and re-run `skills/list` with the + * client's current parameters when refreshed skill metadata is needed. + */ +export type SkillsChangedNotification = Record; diff --git a/src/runtimes/openai/generated/v2/SkillsConfigWriteParams.ts b/src/runtimes/openai/generated/v2/SkillsConfigWriteParams.ts new file mode 100644 index 0000000..81cf2cc --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillsConfigWriteParams.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type SkillsConfigWriteParams = { + /** + * Path-based selector. + */ + path?: AbsolutePathBuf | null; + /** + * Name-based selector. + */ + name?: string | null; + enabled: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/SkillsConfigWriteResponse.ts b/src/runtimes/openai/generated/v2/SkillsConfigWriteResponse.ts new file mode 100644 index 0000000..ba52319 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillsConfigWriteResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillsConfigWriteResponse = { effectiveEnabled: boolean }; diff --git a/src/runtimes/openai/generated/v2/SkillsExtraRootsSetParams.ts b/src/runtimes/openai/generated/v2/SkillsExtraRootsSetParams.ts new file mode 100644 index 0000000..4bb556e --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillsExtraRootsSetParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type SkillsExtraRootsSetParams = { extraRoots: Array }; diff --git a/src/runtimes/openai/generated/v2/SkillsExtraRootsSetResponse.ts b/src/runtimes/openai/generated/v2/SkillsExtraRootsSetResponse.ts new file mode 100644 index 0000000..63be081 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillsExtraRootsSetResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillsExtraRootsSetResponse = Record; diff --git a/src/runtimes/openai/generated/v2/SkillsListEntry.ts b/src/runtimes/openai/generated/v2/SkillsListEntry.ts new file mode 100644 index 0000000..4a0d0b6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillsListEntry.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SkillErrorInfo } from "./SkillErrorInfo"; +import type { SkillMetadata } from "./SkillMetadata"; + +export type SkillsListEntry = { + cwd: string; + skills: Array; + errors: Array; +}; diff --git a/src/runtimes/openai/generated/v2/SkillsListParams.ts b/src/runtimes/openai/generated/v2/SkillsListParams.ts new file mode 100644 index 0000000..9168e75 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillsListParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillsListParams = { + /** + * When empty, defaults to the current session working directory. + */ + cwds?: Array; + /** + * When true, bypass the skills cache and re-scan skills from disk. + */ + forceReload?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/SkillsListResponse.ts b/src/runtimes/openai/generated/v2/SkillsListResponse.ts new file mode 100644 index 0000000..6fdb9a7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SkillsListResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SkillsListEntry } from "./SkillsListEntry"; + +export type SkillsListResponse = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/SortDirection.ts b/src/runtimes/openai/generated/v2/SortDirection.ts new file mode 100644 index 0000000..d8597a4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/SortDirection.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SortDirection = "asc" | "desc"; diff --git a/src/runtimes/openai/generated/v2/SpendControlLimitSnapshot.ts b/src/runtimes/openai/generated/v2/SpendControlLimitSnapshot.ts new file mode 100644 index 0000000..865e95b --- /dev/null +++ b/src/runtimes/openai/generated/v2/SpendControlLimitSnapshot.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SpendControlLimitSnapshot = { + limit: string; + used: string; + remainingPercent: number; + resetsAt: number; +}; diff --git a/src/runtimes/openai/generated/v2/StrictReviewRequiredNotification.ts b/src/runtimes/openai/generated/v2/StrictReviewRequiredNotification.ts new file mode 100644 index 0000000..6a37c51 --- /dev/null +++ b/src/runtimes/openai/generated/v2/StrictReviewRequiredNotification.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type StrictReviewRequiredNotification = { + threadId: string; + turnId: string; + /** + * Unix timestamp (in milliseconds) when this review started. + */ + startedAtMs: number; +}; diff --git a/src/runtimes/openai/generated/v2/SubAgentActivityKind.ts b/src/runtimes/openai/generated/v2/SubAgentActivityKind.ts new file mode 100644 index 0000000..8dcacee --- /dev/null +++ b/src/runtimes/openai/generated/v2/SubAgentActivityKind.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SubAgentActivityKind = "started" | "interacted" | "interrupted" | "completed"; diff --git a/src/runtimes/openai/generated/v2/SubagentMigration.ts b/src/runtimes/openai/generated/v2/SubagentMigration.ts new file mode 100644 index 0000000..b361f8b --- /dev/null +++ b/src/runtimes/openai/generated/v2/SubagentMigration.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SubagentMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/TerminalInteractionNotification.ts b/src/runtimes/openai/generated/v2/TerminalInteractionNotification.ts new file mode 100644 index 0000000..17f4978 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TerminalInteractionNotification.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TerminalInteractionNotification = { + threadId: string; + turnId: string; + itemId: string; + processId: string; + stdin: string; +}; diff --git a/src/runtimes/openai/generated/v2/TextElement.ts b/src/runtimes/openai/generated/v2/TextElement.ts new file mode 100644 index 0000000..e81e531 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TextElement.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ByteRange } from "./ByteRange"; + +export type TextElement = { + /** + * Byte range in the parent `text` buffer that this element occupies. + */ + byteRange: ByteRange; + /** + * Optional human-readable placeholder for the element, displayed in the UI. + */ + placeholder: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/TextPosition.ts b/src/runtimes/openai/generated/v2/TextPosition.ts new file mode 100644 index 0000000..fb6c645 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TextPosition.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TextPosition = { + /** + * 1-based line number. + */ + line: number; + /** + * 1-based column number (in Unicode scalar values). + */ + column: number; +}; diff --git a/src/runtimes/openai/generated/v2/TextRange.ts b/src/runtimes/openai/generated/v2/TextRange.ts new file mode 100644 index 0000000..868ecd1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TextRange.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TextPosition } from "./TextPosition"; + +export type TextRange = { start: TextPosition; end: TextPosition }; diff --git a/src/runtimes/openai/generated/v2/Thread.ts b/src/runtimes/openai/generated/v2/Thread.ts new file mode 100644 index 0000000..e55dbf2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/Thread.ts @@ -0,0 +1,127 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { GitInfo } from "./GitInfo"; +import type { SessionSource } from "./SessionSource"; +import type { ThreadExtra } from "./ThreadExtra"; +import type { ThreadHistoryMode } from "./ThreadHistoryMode"; +import type { ThreadSection } from "./ThreadSection"; +import type { ThreadSource } from "./ThreadSource"; +import type { ThreadStatus } from "./ThreadStatus"; +import type { Turn } from "./Turn"; + +export type Thread = { + /** + * Identifier for this thread. Codex-generated thread IDs are UUIDv7. + */ + id: string; + /** + * Optional implementation-specific thread data. + */ + extra: ThreadExtra | null; + /** + * Session id shared by threads that belong to the same session tree. + */ + sessionId: string; + /** + * Source thread id when this thread was created by forking another thread. + */ + forkedFromId: string | null; + /** + * The ID of the parent thread. This will only be set if this thread is a subagent. + */ + parentThreadId: string | null; + /** + * Usually the first user message in the thread, if available. + */ + preview: string; + /** + * Whether the thread is ephemeral and should not be materialized on disk. + */ + ephemeral: boolean; + /** + * The independently persisted section selected for this thread, if any. + */ + section: ThreadSection | null; + /** + * Unix timestamp in seconds when the thread entered its current section. + */ + sectionEnteredAt: number | null; + /** + * Canonical project assignment owned by app-server, if any. + */ + projectId: string | null; + /** + * Persisted thread history contract selected when this thread was created. + */ + historyMode: ThreadHistoryMode; + /** + * Model provider used for this thread (for example, 'openai'). + */ + modelProvider: string; + /** + * Unix timestamp (in seconds) when the thread was created. + */ + createdAt: number; + /** + * Unix timestamp (in seconds) when the thread was last updated. + */ + updatedAt: number; + /** + * Unix timestamp (in seconds) used for thread recency ordering. + */ + recencyAt: number | null; + /** + * Current runtime status for the thread. + */ + status: ThreadStatus; + /** + * [UNSTABLE] Path to the thread on disk. + */ + path: string | null; + /** + * Working directory captured for the thread. + */ + cwd: AbsolutePathBuf; + /** + * Version of the CLI that created the thread. + */ + cliVersion: string; + /** + * Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). + */ + source: SessionSource; + /** + * Whether the app server accepts direct turn input for this loaded thread. + * `None` means the capability is unavailable, such as for an unloaded stored thread. + */ + canAcceptDirectInput: boolean | null; + /** + * Optional analytics source classification for this thread. + */ + threadSource: ThreadSource | null; + /** + * Optional random unique nickname assigned to an AgentControl-spawned sub-agent. + */ + agentNickname: string | null; + /** + * Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. + */ + agentRole: string | null; + /** + * Optional Git metadata captured when the thread was created. + */ + gitInfo: GitInfo | null; + /** + * Optional user-facing thread title. + */ + name: string | null; + /** + * Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` + * (when `includeTurns` is true) responses. + * For all other responses and notifications returning a Thread, + * the turns field will be an empty list. + */ + turns: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadActiveFlag.ts b/src/runtimes/openai/generated/v2/ThreadActiveFlag.ts new file mode 100644 index 0000000..73c875a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadActiveFlag.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadActiveFlag = "waitingOnApproval" | "waitingOnUserInput"; diff --git a/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionParams.ts b/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionParams.ts new file mode 100644 index 0000000..cc29831 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionParams.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type ThreadApproveGuardianDeniedActionParams = { + threadId: string; + /** + * Serialized `codex_protocol::protocol::GuardianAssessmentEvent`. + */ + event: JsonValue; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionResponse.ts b/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionResponse.ts new file mode 100644 index 0000000..856bb28 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadApproveGuardianDeniedActionResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadArchiveParams.ts b/src/runtimes/openai/generated/v2/ThreadArchiveParams.ts new file mode 100644 index 0000000..81d1d47 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadArchiveParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadArchiveParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadArchiveResponse.ts b/src/runtimes/openai/generated/v2/ThreadArchiveResponse.ts new file mode 100644 index 0000000..b595426 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadArchiveResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadArchiveResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadArchivedNotification.ts b/src/runtimes/openai/generated/v2/ThreadArchivedNotification.ts new file mode 100644 index 0000000..a2e81a7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadArchivedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadArchivedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminal.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminal.ts new file mode 100644 index 0000000..ff49a6c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminal.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LegacyAppPathString } from "../LegacyAppPathString"; + +export type ThreadBackgroundTerminal = { + itemId: string; + processId: string; + command: string; + cwd: LegacyAppPathString; + osPid: number | null; + cpuPercent: number | null; + rssKb: bigint | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanParams.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanParams.ts new file mode 100644 index 0000000..0b8f181 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadBackgroundTerminalsCleanParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanResponse.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanResponse.ts new file mode 100644 index 0000000..f531fe0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadBackgroundTerminalsCleanResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListParams.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListParams.ts new file mode 100644 index 0000000..fcfcd90 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListParams.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadBackgroundTerminalsListParams = { + threadId: string; + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size. + */ + limit?: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListResponse.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListResponse.ts new file mode 100644 index 0000000..227927e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListResponse.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadBackgroundTerminal } from "./ThreadBackgroundTerminal"; + +export type ThreadBackgroundTerminalsListResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * If None, there are no more items to return. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateParams.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateParams.ts new file mode 100644 index 0000000..d6f5388 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadBackgroundTerminalsTerminateParams = { threadId: string; processId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateResponse.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateResponse.ts new file mode 100644 index 0000000..f08b691 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadBackgroundTerminalsTerminateResponse = { terminated: boolean }; diff --git a/src/runtimes/openai/generated/v2/ThreadClosedNotification.ts b/src/runtimes/openai/generated/v2/ThreadClosedNotification.ts new file mode 100644 index 0000000..9efd8c2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadClosedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadClosedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadCompactStartParams.ts b/src/runtimes/openai/generated/v2/ThreadCompactStartParams.ts new file mode 100644 index 0000000..bdb894f --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadCompactStartParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadCompactStartParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadCompactStartResponse.ts b/src/runtimes/openai/generated/v2/ThreadCompactStartResponse.ts new file mode 100644 index 0000000..3794feb --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadCompactStartResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadCompactStartResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadDecrementElicitationParams.ts b/src/runtimes/openai/generated/v2/ThreadDecrementElicitationParams.ts new file mode 100644 index 0000000..3dcdceb --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadDecrementElicitationParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Parameters for `thread/decrement_elicitation`. + */ +export type ThreadDecrementElicitationParams = { + /** + * Thread whose out-of-band elicitation counter should be decremented. + */ + threadId: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadDecrementElicitationResponse.ts b/src/runtimes/openai/generated/v2/ThreadDecrementElicitationResponse.ts new file mode 100644 index 0000000..d0f20a2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadDecrementElicitationResponse.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Response for `thread/decrement_elicitation`. + */ +export type ThreadDecrementElicitationResponse = { + /** + * Current out-of-band elicitation count after the decrement. + */ + count: bigint; + /** + * Whether timeout accounting remains paused after applying the decrement. + */ + paused: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadDeleteParams.ts b/src/runtimes/openai/generated/v2/ThreadDeleteParams.ts new file mode 100644 index 0000000..9f08a79 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadDeleteParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadDeleteParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadDeleteResponse.ts b/src/runtimes/openai/generated/v2/ThreadDeleteResponse.ts new file mode 100644 index 0000000..1af1c30 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadDeleteResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadDeleteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadDeletedNotification.ts b/src/runtimes/openai/generated/v2/ThreadDeletedNotification.ts new file mode 100644 index 0000000..4355ea3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadDeletedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadDeletedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadExtra.ts b/src/runtimes/openai/generated/v2/ThreadExtra.ts new file mode 100644 index 0000000..aa35e45 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadExtra.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Extra app-server data for a thread. + */ +export type ThreadExtra = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadForkParams.ts b/src/runtimes/openai/generated/v2/ThreadForkParams.ts new file mode 100644 index 0000000..534adbb --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadForkParams.ts @@ -0,0 +1,83 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { JsonValue } from "../serde_json/JsonValue"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxMode } from "./SandboxMode"; +import type { ThreadSource } from "./ThreadSource"; + +/** + * There are two ways to fork a thread: + * 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. + * 2. By path: load the thread from disk by path and fork it into a new thread. + * + * If using a non-empty path, the thread_id param will be ignored. + * Empty string path values are treated as absent. + * + * Prefer using thread_id whenever possible. + */ +export type ThreadForkParams = { + threadId: string; + /** + * Optional last turn id to fork through, inclusive. + * + * When specified, turns after `last_turn_id` are omitted from the fork. + * The referenced turn cannot be in progress. + */ + lastTurnId?: string | null; + /** + * Optional turn id to fork before, excluding that turn and all later turns. + * Cannot be combined with `last_turn_id`. + */ + beforeTurnId?: string | null; + /** + * [UNSTABLE] Specify the rollout path to fork from. + * If specified, the thread_id param will be ignored. + */ + path?: string | null; + /** + * Configuration overrides for the forked thread, if any. + */ + model?: string | null; + modelProvider?: string | null; + serviceTier?: string | null | null; + cwd?: string | null; + /** + * Replace the thread's runtime workspace roots. Paths must be absolute. + */ + runtimeWorkspaceRoots?: Array | null; + approvalPolicy?: AskForApproval | null; + /** + * Override where approval requests are routed for review on this thread + * and subsequent turns. + */ + approvalsReviewer?: ApprovalsReviewer | null; + sandbox?: SandboxMode | null; + /** + * Named profile id for the forked thread. Cannot be combined with + * `sandbox`. + */ + permissions?: string | null; + config?: { [key in string]?: JsonValue } | null; + baseInstructions?: string | null; + developerInstructions?: string | null; + ephemeral?: boolean; + /** + * Optional client-supplied analytics source classification for this forked thread. + */ + threadSource?: ThreadSource | null; + /** + * When true, return only thread metadata and live fork state without + * populating `thread.turns`. This is useful when the client plans to call + * `thread/turns/list` immediately after forking. + */ + excludeTurns?: boolean; + /** + * When true, carry the source thread's current goal into the fork without + * starting its initial automatic continuation. The next explicit turn owns + * the goal lifecycle, and normal automatic continuation resumes after it. + */ + deferGoalContinuation?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadForkResponse.ts b/src/runtimes/openai/generated/v2/ThreadForkResponse.ts new file mode 100644 index 0000000..0b93c18 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadForkResponse.ts @@ -0,0 +1,49 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { LegacyAppPathString } from "../LegacyAppPathString"; +import type { MultiAgentMode } from "../MultiAgentMode"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ActivePermissionProfile } from "./ActivePermissionProfile"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxPolicy } from "./SandboxPolicy"; +import type { Thread } from "./Thread"; + +export type ThreadForkResponse = { + thread: Thread; + model: string; + modelProvider: string; + serviceTier: string | null; + cwd: AbsolutePathBuf; + /** + * Thread-scoped runtime workspace roots used to materialize + * `:workspace_roots`. + */ + runtimeWorkspaceRoots: Array; + /** + * Environment-native paths to instruction source files currently loaded for this thread. + */ + instructionSources: Array; + approvalPolicy: AskForApproval; + /** + * Reviewer currently used for approval requests on this thread. + */ + approvalsReviewer: ApprovalsReviewer; + /** + * Legacy sandbox policy retained for compatibility. Experimental clients + * should prefer `activePermissionProfile` for profile provenance. + */ + sandbox: SandboxPolicy; + /** + * Named or implicit built-in profile that produced the active + * permissions, when known. + */ + activePermissionProfile: ActivePermissionProfile | null; + reasoningEffort: ReasoningEffort | null; + /** + * @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. + */ + multiAgentMode: MultiAgentMode; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadGoal.ts b/src/runtimes/openai/generated/v2/ThreadGoal.ts new file mode 100644 index 0000000..06274ca --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoal.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoalStatus } from "./ThreadGoalStatus"; + +export type ThreadGoal = { + threadId: string; + objective: string; + status: ThreadGoalStatus; + tokenBudget: number | null; + tokensUsed: number; + timeUsedSeconds: number; + createdAt: number; + updatedAt: number; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalClearParams.ts b/src/runtimes/openai/generated/v2/ThreadGoalClearParams.ts new file mode 100644 index 0000000..f12b4b8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoalClearParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadGoalClearParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalClearResponse.ts b/src/runtimes/openai/generated/v2/ThreadGoalClearResponse.ts new file mode 100644 index 0000000..3d17dd9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoalClearResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadGoalClearResponse = { cleared: boolean }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalClearedNotification.ts b/src/runtimes/openai/generated/v2/ThreadGoalClearedNotification.ts new file mode 100644 index 0000000..ecb15e4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoalClearedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadGoalClearedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalGetParams.ts b/src/runtimes/openai/generated/v2/ThreadGoalGetParams.ts new file mode 100644 index 0000000..334444e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoalGetParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadGoalGetParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalGetResponse.ts b/src/runtimes/openai/generated/v2/ThreadGoalGetResponse.ts new file mode 100644 index 0000000..b82d7a9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoalGetResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoal } from "./ThreadGoal"; + +export type ThreadGoalGetResponse = { goal: ThreadGoal | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalSetParams.ts b/src/runtimes/openai/generated/v2/ThreadGoalSetParams.ts new file mode 100644 index 0000000..d7d7914 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoalSetParams.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoalStatus } from "./ThreadGoalStatus"; + +export type ThreadGoalSetParams = { + threadId: string; + objective?: string | null; + status?: ThreadGoalStatus | null; + tokenBudget?: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalSetResponse.ts b/src/runtimes/openai/generated/v2/ThreadGoalSetResponse.ts new file mode 100644 index 0000000..78a25e0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoalSetResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoal } from "./ThreadGoal"; + +export type ThreadGoalSetResponse = { goal: ThreadGoal }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalStatus.ts b/src/runtimes/openai/generated/v2/ThreadGoalStatus.ts new file mode 100644 index 0000000..5a897f2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoalStatus.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadGoalStatus = + | "active" + | "paused" + | "blocked" + | "usageLimited" + | "budgetLimited" + | "complete"; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadGoalUpdatedNotification.ts new file mode 100644 index 0000000..f4befc1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadGoalUpdatedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoal } from "./ThreadGoal"; + +export type ThreadGoalUpdatedNotification = { + threadId: string; + turnId: string | null; + goal: ThreadGoal; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadHistoryMode.ts b/src/runtimes/openai/generated/v2/ThreadHistoryMode.ts new file mode 100644 index 0000000..db0f2d8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadHistoryMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadHistoryMode = "legacy" | "paginated"; diff --git a/src/runtimes/openai/generated/v2/ThreadIncrementElicitationParams.ts b/src/runtimes/openai/generated/v2/ThreadIncrementElicitationParams.ts new file mode 100644 index 0000000..12208cc --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadIncrementElicitationParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Parameters for `thread/increment_elicitation`. + */ +export type ThreadIncrementElicitationParams = { + /** + * Thread whose out-of-band elicitation counter should be incremented. + */ + threadId: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadIncrementElicitationResponse.ts b/src/runtimes/openai/generated/v2/ThreadIncrementElicitationResponse.ts new file mode 100644 index 0000000..eed5c35 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadIncrementElicitationResponse.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Response for `thread/increment_elicitation`. + */ +export type ThreadIncrementElicitationResponse = { + /** + * Current out-of-band elicitation count after the increment. + */ + count: bigint; + /** + * Whether timeout accounting is paused after applying the increment. + */ + paused: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadInjectItemsParams.ts b/src/runtimes/openai/generated/v2/ThreadInjectItemsParams.ts new file mode 100644 index 0000000..34f1513 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadInjectItemsParams.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type ThreadInjectItemsParams = { + threadId: string; + /** + * Raw Responses API items to append to the thread's model-visible history. + */ + items: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadInjectItemsResponse.ts b/src/runtimes/openai/generated/v2/ThreadInjectItemsResponse.ts new file mode 100644 index 0000000..60dcf0d --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadInjectItemsResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadInjectItemsResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadItem.ts b/src/runtimes/openai/generated/v2/ThreadItem.ts new file mode 100644 index 0000000..34a003a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadItem.ts @@ -0,0 +1,177 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ImageGenerationItem } from "../ImageGenerationItem"; +import type { LegacyAppPathString } from "../LegacyAppPathString"; +import type { MessagePhase } from "../MessagePhase"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { SleepItem } from "../SleepItem"; +import type { WebSearchItem } from "../WebSearchItem"; +import type { JsonValue } from "../serde_json/JsonValue"; +import type { AgentMessageDelivery } from "./AgentMessageDelivery"; +import type { CollabAgentState } from "./CollabAgentState"; +import type { CollabAgentTool } from "./CollabAgentTool"; +import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; +import type { CommandAction } from "./CommandAction"; +import type { CommandExecutionSource } from "./CommandExecutionSource"; +import type { CommandExecutionStatus } from "./CommandExecutionStatus"; +import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; +import type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; +import type { FileUpdateChange } from "./FileUpdateChange"; +import type { HookPromptFragment } from "./HookPromptFragment"; +import type { McpToolCallAppContext } from "./McpToolCallAppContext"; +import type { McpToolCallError } from "./McpToolCallError"; +import type { McpToolCallResult } from "./McpToolCallResult"; +import type { McpToolCallStatus } from "./McpToolCallStatus"; +import type { MemoryCitation } from "./MemoryCitation"; +import type { PatchApplyStatus } from "./PatchApplyStatus"; +import type { SubAgentActivityKind } from "./SubAgentActivityKind"; +import type { UserInput } from "./UserInput"; + +export type ThreadItem = + | { type: "userMessage"; id: string; clientId: string | null; content: Array } + | { type: "hookPrompt"; id: string; fragments: Array } + | { + type: "agentMessage"; + id: string; + text: string; + phase: MessagePhase | null; + memoryCitation: MemoryCitation | null; + delivery: AgentMessageDelivery | null; + } + | { type: "plan"; id: string; text: string } + | { type: "reasoning"; id: string; summary: Array; content: Array } + | { + type: "commandExecution"; + id: string; + /** + * Trusted first-party plugin id when this command resolves to one plugin script. + */ + pluginId: string | null; + /** + * Safe plugin-relative path when this command resolves to one plugin script. + */ + scriptPath: string | null; + /** + * The command to be executed. + */ + command: string; + /** + * The command's working directory. + */ + cwd: LegacyAppPathString; + /** + * Identifier for the underlying PTY process (when available). + */ + processId: string | null; + source: CommandExecutionSource; + status: CommandExecutionStatus; + /** + * A best-effort parsing of the command to understand the action(s) it will perform. + * This returns a list of CommandAction objects because a single shell command may + * be composed of many commands piped together. + */ + commandActions: Array; + /** + * The command's output, aggregated from stdout and stderr. + */ + aggregatedOutput: string | null; + /** + * The command's exit code. + */ + exitCode: number | null; + /** + * The duration of the command execution in milliseconds. + */ + durationMs: number | null; + } + | { type: "fileChange"; id: string; changes: Array; status: PatchApplyStatus } + | { + type: "mcpToolCall"; + id: string; + server: string; + tool: string; + status: McpToolCallStatus; + arguments: JsonValue; + appContext: McpToolCallAppContext | null; + /** + * Deprecated: use `appContext.resourceUri` instead. + */ + mcpAppResourceUri?: string; + pluginId: string | null; + readOnlyHint: boolean | null; + result: McpToolCallResult | null; + error: McpToolCallError | null; + /** + * The duration of the MCP tool call in milliseconds. + */ + durationMs: number | null; + } + | { + type: "dynamicToolCall"; + id: string; + namespace: string | null; + tool: string; + arguments: JsonValue; + status: DynamicToolCallStatus; + contentItems: Array | null; + success: boolean | null; + /** + * The duration of the dynamic tool call in milliseconds. + */ + durationMs: number | null; + } + | { + type: "collabAgentToolCall"; + /** + * Unique identifier for this collab tool call. + */ + id: string; + /** + * Name of the collab tool that was invoked. + */ + tool: CollabAgentTool; + /** + * Current status of the collab tool call. + */ + status: CollabAgentToolCallStatus; + /** + * Thread ID of the agent issuing the collab request. + */ + senderThreadId: string; + /** + * Thread ID of the receiving agent, when applicable. In case of spawn operation, + * this corresponds to the newly spawned agent. + */ + receiverThreadIds: Array; + /** + * Prompt text sent as part of the collab tool call, when available. + */ + prompt: string | null; + /** + * Model requested for the spawned agent, when applicable. + */ + model: string | null; + /** + * Reasoning effort requested for the spawned agent, when applicable. + */ + reasoningEffort: ReasoningEffort | null; + /** + * Last known status of the target agents, when available. + */ + agentsStates: { [key in string]?: CollabAgentState }; + } + | { + type: "subAgentActivity"; + id: string; + kind: SubAgentActivityKind; + agentThreadId: string; + agentPath: string; + } + | ({ type: "webSearch" } & WebSearchItem) + | { type: "imageView"; id: string; path: LegacyAppPathString } + | ({ type: "sleep" } & SleepItem) + | ({ type: "imageGeneration" } & ImageGenerationItem) + | { type: "enteredReviewMode"; id: string; review: string } + | { type: "exitedReviewMode"; id: string; review: string } + | { type: "contextCompaction"; id: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadItemEntry.ts b/src/runtimes/openai/generated/v2/ThreadItemEntry.ts new file mode 100644 index 0000000..d78531c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadItemEntry.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadItem } from "./ThreadItem"; + +export type ThreadItemEntry = { + /** + * Turn containing this item. + */ + turnId: string; + item: ThreadItem; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadItemsListParams.ts b/src/runtimes/openai/generated/v2/ThreadItemsListParams.ts new file mode 100644 index 0000000..e6ec19b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadItemsListParams.ts @@ -0,0 +1,24 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SortDirection } from "./SortDirection"; + +export type ThreadItemsListParams = { + threadId: string; + /** + * Optional turn id to filter by. When omitted, returns items across the thread. + */ + turnId?: string | null; + /** + * Opaque cursor to pass to the next call to continue after the last item. + */ + cursor?: string | null; + /** + * Optional item page size. + */ + limit?: number | null; + /** + * Optional item pagination direction; defaults to ascending. + */ + sortDirection?: SortDirection | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadItemsListResponse.ts b/src/runtimes/openai/generated/v2/ThreadItemsListResponse.ts new file mode 100644 index 0000000..d0804cb --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadItemsListResponse.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadItemEntry } from "./ThreadItemEntry"; + +export type ThreadItemsListResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * if None, there are no more items to return. + */ + nextCursor: string | null; + /** + * Opaque cursor to pass as `cursor` when reversing `sortDirection`. + * This is only populated when the page contains at least one item. + */ + backwardsCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadListParams.ts b/src/runtimes/openai/generated/v2/ThreadListParams.ts new file mode 100644 index 0000000..0000d80 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadListParams.ts @@ -0,0 +1,74 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SortDirection } from "./SortDirection"; +import type { ThreadSortKey } from "./ThreadSortKey"; +import type { ThreadSourceKind } from "./ThreadSourceKind"; + +export type ThreadListParams = { + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size; defaults to a reasonable server-side value. + */ + limit?: number | null; + /** + * Optional sort key; defaults to created_at. + */ + sortKey?: ThreadSortKey | null; + /** + * Optional sort direction; defaults to descending (newest first). + */ + sortDirection?: SortDirection | null; + /** + * Optional provider filter; when set, only sessions recorded under these + * providers are returned. When present but empty, includes all providers. + */ + modelProviders?: Array | null; + /** + * Optional source filter; when set, only sessions from these source kinds + * are returned. When omitted or empty, defaults to interactive sources. + */ + sourceKinds?: Array | null; + /** + * Optional archived filter; when set to true, only archived threads are returned. + * If false or null, only non-archived threads are returned. + */ + archived?: boolean | null; + /** + * Omit to include every section, set to `null` for unsectioned threads, + * or provide a section ID to return only threads in that section. + */ + sectionId?: string | null; + /** + * Omit to include every project, set to null for unassigned threads, + * or provide a project ID to return only threads in that project. + */ + projectId?: string | null; + /** + * Optional cwd filter or filters; when set, only threads whose session cwd + * exactly matches one of these paths are returned. + */ + cwd?: string | Array | null; + /** + * If true, return from the state DB without scanning JSONL rollouts to + * repair thread metadata. Omitted or false preserves scan-and-repair + * behavior. + */ + useStateDbOnly?: boolean; + /** + * Optional substring filter for the extracted thread title. + */ + searchTerm?: string | null; + /** + * Optional direct parent thread filter. Mutually exclusive with `ancestorThreadId`. + */ + parentThreadId?: string | null; + /** + * Optional ancestor thread filter. Returns spawned descendants at any depth, excluding the + * ancestor itself. Mutually exclusive with `parentThreadId`. + */ + ancestorThreadId?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadListResponse.ts b/src/runtimes/openai/generated/v2/ThreadListResponse.ts new file mode 100644 index 0000000..4496da4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadListResponse.ts @@ -0,0 +1,20 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Thread } from "./Thread"; + +export type ThreadListResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * if None, there are no more items to return. + */ + nextCursor: string | null; + /** + * Opaque cursor to pass as `cursor` when reversing `sortDirection`. + * This is only populated when the page contains at least one thread. + * Use it with the opposite `sortDirection`; for timestamp sorts it anchors + * at the start of the page timestamp so same-second updates are not skipped. + */ + backwardsCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadLoadedListParams.ts b/src/runtimes/openai/generated/v2/ThreadLoadedListParams.ts new file mode 100644 index 0000000..7269540 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadLoadedListParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadLoadedListParams = { + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size; defaults to no limit. + */ + limit?: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadLoadedListResponse.ts b/src/runtimes/openai/generated/v2/ThreadLoadedListResponse.ts new file mode 100644 index 0000000..5555ac2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadLoadedListResponse.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadLoadedListResponse = { + /** + * Thread ids for sessions currently loaded in memory. + */ + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * if None, there are no more items to return. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadMemoryModeSetParams.ts b/src/runtimes/openai/generated/v2/ThreadMemoryModeSetParams.ts new file mode 100644 index 0000000..f4ec0a6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadMemoryModeSetParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadMemoryMode } from "../ThreadMemoryMode"; + +export type ThreadMemoryModeSetParams = { threadId: string; mode: ThreadMemoryMode }; diff --git a/src/runtimes/openai/generated/v2/ThreadMemoryModeSetResponse.ts b/src/runtimes/openai/generated/v2/ThreadMemoryModeSetResponse.ts new file mode 100644 index 0000000..49b42fd --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadMemoryModeSetResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadMemoryModeSetResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadMetadataGitInfoUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadMetadataGitInfoUpdateParams.ts new file mode 100644 index 0000000..391427c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadMetadataGitInfoUpdateParams.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadMetadataGitInfoUpdateParams = { + /** + * Omit to leave the stored commit unchanged, set to `null` to clear it, + * or provide a non-empty string to replace it. + */ + sha?: string | null; + /** + * Omit to leave the stored branch unchanged, set to `null` to clear it, + * or provide a non-empty string to replace it. + */ + branch?: string | null; + /** + * Omit to leave the stored origin URL unchanged, set to `null` to clear it, + * or provide a non-empty string to replace it. + */ + originUrl?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadMetadataUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadMetadataUpdateParams.ts new file mode 100644 index 0000000..ad18c86 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadMetadataUpdateParams.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadMetadataGitInfoUpdateParams } from "./ThreadMetadataGitInfoUpdateParams"; + +export type ThreadMetadataUpdateParams = { + threadId: string; + /** + * Omit to leave the project unchanged, use an empty string to clear it, + * or provide an existing project ID to assign it. + */ + projectId?: string | null; + /** + * Patch the stored Git metadata for this thread. + * Omit a field to leave it unchanged, set it to `null` to clear it, or + * provide a string to replace the stored value. + */ + gitInfo?: ThreadMetadataGitInfoUpdateParams | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadMetadataUpdateResponse.ts b/src/runtimes/openai/generated/v2/ThreadMetadataUpdateResponse.ts new file mode 100644 index 0000000..9b1b298 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadMetadataUpdateResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Thread } from "./Thread"; + +export type ThreadMetadataUpdateResponse = { thread: Thread }; diff --git a/src/runtimes/openai/generated/v2/ThreadNameUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadNameUpdatedNotification.ts new file mode 100644 index 0000000..1bee8b5 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadNameUpdatedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadNameUpdatedNotification = { threadId: string; threadName?: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadProjectUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadProjectUpdatedNotification.ts new file mode 100644 index 0000000..1d882b1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadProjectUpdatedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadProjectUpdatedNotification = { threadId: string; projectId: string | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueAddParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueAddParams.ts new file mode 100644 index 0000000..a9b03c9 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueAddParams.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { UserInput } from "./UserInput"; + +export type ThreadQueueAddParams = { + threadId: string; + input: Array; + clientUserMessageId: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueAddResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueAddResponse.ts new file mode 100644 index 0000000..fd7c088 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueAddResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { QueuedSubmission } from "./QueuedSubmission"; + +export type ThreadQueueAddResponse = { queuedSubmission: QueuedSubmission }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueChangedNotification.ts b/src/runtimes/openai/generated/v2/ThreadQueueChangedNotification.ts new file mode 100644 index 0000000..a6ff3b5 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueChangedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadQueueChangedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueDeleteParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueDeleteParams.ts new file mode 100644 index 0000000..d0e8f3a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueDeleteParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadQueueDeleteParams = { threadId: string; queuedSubmissionId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueDeleteResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueDeleteResponse.ts new file mode 100644 index 0000000..9f3f48a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueDeleteResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadQueueDeleteResponse = { deleted: boolean }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueListParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueListParams.ts new file mode 100644 index 0000000..fe23183 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueListParams.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadQueueListParams = { + threadId: string; + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size; defaults to the standard thread-list page size. + */ + limit?: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueListResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueListResponse.ts new file mode 100644 index 0000000..077bf4d --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueListResponse.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { QueuedSubmission } from "./QueuedSubmission"; + +export type ThreadQueueListResponse = { + data: Array; + /** + * Opaque cursor for the next page, or `null` when no submissions remain. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueReorderParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueReorderParams.ts new file mode 100644 index 0000000..86a98f1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueReorderParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadQueueReorderParams = { threadId: string; queuedSubmissionIds: Array }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueReorderResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueReorderResponse.ts new file mode 100644 index 0000000..3208aa5 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueReorderResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadQueueReorderResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueStartParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueStartParams.ts new file mode 100644 index 0000000..cf2bed1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueStartParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadQueueStartParams = { threadId: string; queuedSubmissionId?: string | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueStartResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueStartResponse.ts new file mode 100644 index 0000000..b0f17c8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueStartResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Turn } from "./Turn"; + +export type ThreadQueueStartResponse = { turn: Turn }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueUpdateParams.ts new file mode 100644 index 0000000..39ee6e7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueUpdateParams.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { UserInput } from "./UserInput"; + +export type ThreadQueueUpdateParams = { + threadId: string; + queuedSubmissionId: string; + input: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueUpdateResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueUpdateResponse.ts new file mode 100644 index 0000000..ac663a6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadQueueUpdateResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { QueuedSubmission } from "./QueuedSubmission"; + +export type ThreadQueueUpdateResponse = { queuedSubmission: QueuedSubmission }; diff --git a/src/runtimes/openai/generated/v2/ThreadReadParams.ts b/src/runtimes/openai/generated/v2/ThreadReadParams.ts new file mode 100644 index 0000000..b088639 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadReadParams.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadReadParams = { + threadId: string; + /** + * When true, include turns and their items from rollout history. + */ + includeTurns?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadReadResponse.ts b/src/runtimes/openai/generated/v2/ThreadReadResponse.ts new file mode 100644 index 0000000..2bbdd1a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadReadResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Thread } from "./Thread"; + +export type ThreadReadResponse = { thread: Thread }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioParams.ts new file mode 100644 index 0000000..0d0867e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioParams.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk"; + +/** + * EXPERIMENTAL - append audio input to thread realtime. + */ +export type ThreadRealtimeAppendAudioParams = { threadId: string; audio: ThreadRealtimeAudioChunk }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioResponse.ts new file mode 100644 index 0000000..063e8cb --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - response for appending realtime audio input. + */ +export type ThreadRealtimeAppendAudioResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechParams.ts new file mode 100644 index 0000000..6839405 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - append speakable text to thread realtime. + */ +export type ThreadRealtimeAppendSpeechParams = { threadId: string; text: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechResponse.ts new file mode 100644 index 0000000..4963999 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - response for appending realtime speech. + */ +export type ThreadRealtimeAppendSpeechResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextParams.ts new file mode 100644 index 0000000..f1ce8d7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConversationTextRole } from "../ConversationTextRole"; + +/** + * EXPERIMENTAL - append text input to thread realtime. + */ +export type ThreadRealtimeAppendTextParams = { + threadId: string; + text: string; + role: ConversationTextRole; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextResponse.ts new file mode 100644 index 0000000..1fb9f07 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - response for appending realtime text input. + */ +export type ThreadRealtimeAppendTextResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAudioChunk.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAudioChunk.ts new file mode 100644 index 0000000..61f4414 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeAudioChunk.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - thread realtime audio chunk. + */ +export type ThreadRealtimeAudioChunk = { + data: string; + sampleRate: number; + numChannels: number; + samplesPerChannel: number | null; + itemId: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeBemItemPresentation.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeBemItemPresentation.ts new file mode 100644 index 0000000..f59d512 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeBemItemPresentation.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - how an existing agent item appears in a realtime conversation. + */ +export type ThreadRealtimeBemItemPresentation = + | { type: "wholeItem" } + | { type: "inlineMarkdown" } + | { type: "inlineVisualization"; index: number }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeClosedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeClosedNotification.ts new file mode 100644 index 0000000..7f7e837 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeClosedNotification.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - emitted when thread realtime transport closes. + */ +export type ThreadRealtimeClosedNotification = { threadId: string; reason: string | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeErrorNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeErrorNotification.ts new file mode 100644 index 0000000..3c1e9d4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeErrorNotification.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - emitted when thread realtime encounters an error. + */ +export type ThreadRealtimeErrorNotification = { threadId: string; message: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeInitialItem.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeInitialItem.ts new file mode 100644 index 0000000..c765f31 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeInitialItem.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConversationTextRole } from "../ConversationTextRole"; + +/** + * EXPERIMENTAL - role-bearing text item included when a realtime V3 session starts. + */ +export type ThreadRealtimeInitialItem = { role: ConversationTextRole; text: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItem.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItem.ts new file mode 100644 index 0000000..7fcc1d7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeItem.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadRealtimeBemItemPresentation } from "./ThreadRealtimeBemItemPresentation"; +import type { ThreadRealtimeSessionOutcome } from "./ThreadRealtimeSessionOutcome"; +import type { ThreadRealtimeTranscriptRole } from "./ThreadRealtimeTranscriptRole"; + +/** + * EXPERIMENTAL - a thread-scoped realtime item in the canonical timeline. + */ +export type ThreadRealtimeItem = { id: string; realtimeSessionId: string } & ( + | { type: "realtimeSessionStarted" } + | { type: "transcriptSegment"; role: ThreadRealtimeTranscriptRole; text: string } + | { + type: "bemItemPromoted"; + turnId: string; + itemId: string; + presentation: ThreadRealtimeBemItemPresentation; + } + | { type: "realtimeSessionClosed"; outcome: ThreadRealtimeSessionOutcome } +); diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItemAddedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItemAddedNotification.ts new file mode 100644 index 0000000..8f2c1b1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeItemAddedNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +/** + * EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend. + */ +export type ThreadRealtimeItemAddedNotification = { threadId: string; item: JsonValue }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItemCompletedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItemCompletedNotification.ts new file mode 100644 index 0000000..902551a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeItemCompletedNotification.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadRealtimeItem } from "./ThreadRealtimeItem"; + +/** + * EXPERIMENTAL - a realtime timeline item published after canonical commit. + */ +export type ThreadRealtimeItemCompletedNotification = { + threadId: string; + item: ThreadRealtimeItem; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItemStartedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItemStartedNotification.ts new file mode 100644 index 0000000..e51b63d --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeItemStartedNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadRealtimeItem } from "./ThreadRealtimeItem"; + +/** + * EXPERIMENTAL - a realtime timeline item started before its content streams. + */ +export type ThreadRealtimeItemStartedNotification = { threadId: string; item: ThreadRealtimeItem }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItemTranscriptDeltaNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItemTranscriptDeltaNotification.ts new file mode 100644 index 0000000..500192a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeItemTranscriptDeltaNotification.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - text appended to an active realtime transcript item. + */ +export type ThreadRealtimeItemTranscriptDeltaNotification = { + threadId: string; + itemId: string; + delta: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesParams.ts new file mode 100644 index 0000000..b456d89 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - list voices supported by thread realtime. + */ +export type ThreadRealtimeListVoicesParams = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesResponse.ts new file mode 100644 index 0000000..7e7cc30 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RealtimeVoicesList } from "../RealtimeVoicesList"; + +/** + * EXPERIMENTAL - response for listing supported realtime voices. + */ +export type ThreadRealtimeListVoicesResponse = { voices: RealtimeVoicesList }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeOutputAudioDeltaNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeOutputAudioDeltaNotification.ts new file mode 100644 index 0000000..e4cca9d --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeOutputAudioDeltaNotification.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk"; + +/** + * EXPERIMENTAL - streamed output audio emitted by thread realtime. + */ +export type ThreadRealtimeOutputAudioDeltaNotification = { + threadId: string; + audio: ThreadRealtimeAudioChunk; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeSdpNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeSdpNotification.ts new file mode 100644 index 0000000..38af1bc --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeSdpNotification.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session. + */ +export type ThreadRealtimeSdpNotification = { threadId: string; sdp: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeSessionOutcome.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeSessionOutcome.ts new file mode 100644 index 0000000..c3c73df --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeSessionOutcome.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadRealtimeSessionOutcome = "ended" | "failed"; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStartParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStartParams.ts new file mode 100644 index 0000000..7622c43 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeStartParams.ts @@ -0,0 +1,84 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CodexResponseHandoffMode } from "../CodexResponseHandoffMode"; +import type { RealtimeConversationVersion } from "../RealtimeConversationVersion"; +import type { RealtimeOutputModality } from "../RealtimeOutputModality"; +import type { RealtimeVoice } from "../RealtimeVoice"; +import type { ThreadRealtimeInitialItem } from "./ThreadRealtimeInitialItem"; +import type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTransport"; + +/** + * EXPERIMENTAL - start a thread-scoped realtime session. + */ +export type ThreadRealtimeStartParams = { + threadId: string; + /** + * Leaves Codex response handoffs to the client's explicit append calls instead of forwarding + * them automatically. Defaults to false. + */ + clientManagedHandoffs?: boolean | null; + /** + * Controls whether a realtime V3 delegation produces an acknowledgement filler. + * Omitted values preserve the Realtime API's default behavior. + */ + delegationAckFiller?: boolean | null; + /** + * Routes any transcript tail remaining at session end through Codex. Defaults to false. + * TODO: Remove this rollout knob once transcript-tail flushing is always enabled. + */ + flushTranscriptTailOnSessionEnd?: boolean | null; + /** + * Sends automatic Codex responses as realtime conversation items instead of handoff appends. + */ + codexResponsesAsItems?: boolean | null; + /** + * Optional prefix added to automatic Codex response items when `codexResponsesAsItems` is true. + */ + codexResponseItemPrefix?: string | null; + /** + * Selects how automatic Codex responses are routed in Frameless Bidi sessions. Omitted values + * default to `thinking`. Realtime V1 and V2 ignore this setting. + */ + codexResponseHandoffMode?: CodexResponseHandoffMode | null; + /** + * Overrides BEM channel prefixes by `analysis`, `commentary`, or `final`. + * Omitted channels retain their default uppercase bracketed prefixes. + */ + codexResponseHandoffChannelPrefixes?: { [key in string]?: Array } | null; + /** + * Overrides the configured realtime model for this session only. + */ + model?: string | null; + /** + * Selects text or audio output for the realtime session. Transport and voice stay + * independent so clients can choose how they connect separately from what the model emits. + */ + outputModality: RealtimeOutputModality; + /** + * Set to false to start without Codex's startup context. Omitted or null includes it. + */ + includeStartupContext?: boolean | null; + /** + * Adds complete role-bearing text items to the initial Frameless Bidi session history. + * This is only supported by realtime V3 and is sent during session startup. Requests are + * limited to 128 items and 8,192 estimated text tokens in total. + */ + initialItems?: Array | null; + /** + * Developer instructions given to the backing Codex model when this realtime session starts. + */ + realtimeStartInstructions?: string | null; + /** + * Developer instructions given to the backing Codex model when this realtime session ends. + */ + realtimeEndInstructions?: string | null; + prompt?: string | null | null; + realtimeSessionId?: string | null; + transport?: ThreadRealtimeStartTransport | null; + /** + * Overrides the configured realtime protocol version for this session only. + */ + version?: RealtimeConversationVersion | null; + voice?: RealtimeVoice | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStartResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStartResponse.ts new file mode 100644 index 0000000..5625456 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeStartResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - response for starting thread realtime. + */ +export type ThreadRealtimeStartResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStartTransport.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStartTransport.ts new file mode 100644 index 0000000..2da052b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeStartTransport.ts @@ -0,0 +1,24 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - transport used by thread realtime. + */ +export type ThreadRealtimeStartTransport = + | { type: "websocket" } + | { + type: "webrtc"; + /** + * SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the + * realtime events data channel. + */ + sdp: string; + } + | { + type: "existingCall"; + /** + * Identifier of a realtime call already created and negotiated by the client. + */ + callId: string; + }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStartedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStartedNotification.ts new file mode 100644 index 0000000..2e2ab72 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeStartedNotification.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RealtimeConversationVersion } from "../RealtimeConversationVersion"; + +/** + * EXPERIMENTAL - emitted when thread realtime startup is accepted. + */ +export type ThreadRealtimeStartedNotification = { + threadId: string; + realtimeSessionId: string | null; + version: RealtimeConversationVersion; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStopParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStopParams.ts new file mode 100644 index 0000000..a74f6d8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeStopParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - stop thread realtime. + */ +export type ThreadRealtimeStopParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStopResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStopResponse.ts new file mode 100644 index 0000000..c87f440 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeStopResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - response for stopping thread realtime. + */ +export type ThreadRealtimeStopResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDeltaNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDeltaNotification.ts new file mode 100644 index 0000000..0e6e0f4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDeltaNotification.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - flat transcript delta emitted whenever realtime + * transcript text changes. + */ +export type ThreadRealtimeTranscriptDeltaNotification = { + threadId: string; + role: string; + /** + * Live transcript delta from the realtime event. + */ + delta: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDoneNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDoneNotification.ts new file mode 100644 index 0000000..eafdbe7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDoneNotification.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - final transcript text emitted when realtime completes + * a transcript part. + */ +export type ThreadRealtimeTranscriptDoneNotification = { + threadId: string; + role: string; + /** + * Final complete text for the transcript part. + */ + text: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptRole.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptRole.ts new file mode 100644 index 0000000..d957a42 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptRole.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadRealtimeTranscriptRole = "user" | "assistant"; diff --git a/src/runtimes/openai/generated/v2/ThreadResumeInitialTurnsPageParams.ts b/src/runtimes/openai/generated/v2/ThreadResumeInitialTurnsPageParams.ts new file mode 100644 index 0000000..096e993 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadResumeInitialTurnsPageParams.ts @@ -0,0 +1,20 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SortDirection } from "./SortDirection"; +import type { TurnItemsView } from "./TurnItemsView"; + +export type ThreadResumeInitialTurnsPageParams = { + /** + * Optional turn page size. + */ + limit?: number | null; + /** + * Optional turn pagination direction; defaults to descending. + */ + sortDirection?: SortDirection | null; + /** + * How much item detail to include for each returned turn; defaults to summary. + */ + itemsView?: TurnItemsView | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadResumeParams.ts b/src/runtimes/openai/generated/v2/ThreadResumeParams.ts new file mode 100644 index 0000000..be72532 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadResumeParams.ts @@ -0,0 +1,82 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { Personality } from "../Personality"; +import type { ResponseItem } from "../ResponseItem"; +import type { JsonValue } from "../serde_json/JsonValue"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxMode } from "./SandboxMode"; +import type { ThreadResumeInitialTurnsPageParams } from "./ThreadResumeInitialTurnsPageParams"; + +/** + * There are three ways to resume a thread: + * 1. By thread_id: load the thread from disk by thread_id and resume it. + * 2. By history: instantiate the thread from memory and resume it. + * 3. By path: load the thread from disk by path and resume it. + * + * For non-running threads, the precedence is: history > non-empty path > thread_id. + * If using history or a non-empty path for a non-running thread, the thread_id + * param will be ignored. + * + * If thread_id identifies a running thread, app-server rejoins that thread and + * treats a non-empty path as a consistency check against the active rollout path. + * Empty string path values are treated as absent. + * + * Prefer using thread_id whenever possible. + */ +export type ThreadResumeParams = { + threadId: string; + /** + * [UNSTABLE] FOR CODEX CLOUD - DO NOT USE. + * If specified, the thread will be resumed with the provided history + * instead of loaded from disk. + */ + history?: Array | null; + /** + * [UNSTABLE] Specify the rollout path to resume from. + * If specified for a non-running thread, the thread_id param will be ignored. + * If thread_id identifies a running thread, the path must match the active + * rollout path. + */ + path?: string | null; + /** + * Configuration overrides for the resumed thread, if any. + */ + model?: string | null; + modelProvider?: string | null; + serviceTier?: string | null | null; + cwd?: string | null; + /** + * Replace the thread's runtime workspace roots. Paths must be absolute. + */ + runtimeWorkspaceRoots?: Array | null; + approvalPolicy?: AskForApproval | null; + /** + * Override where approval requests are routed for review on this thread + * and subsequent turns. + */ + approvalsReviewer?: ApprovalsReviewer | null; + sandbox?: SandboxMode | null; + /** + * Named profile id for the resumed thread. Cannot be combined with + * `sandbox`. + */ + permissions?: string | null; + config?: { [key in string]?: JsonValue } | null; + baseInstructions?: string | null; + developerInstructions?: string | null; + personality?: Personality | null; + /** + * When true, return only thread metadata and live-resume state without + * populating `thread.turns`. This is useful when the client plans to call + * `thread/turns/list` immediately after resuming. + */ + excludeTurns?: boolean; + /** + * When present, include a `thread/turns/list` page in the resume response + * so clients can bootstrap recent turns without a second request. + */ + initialTurnsPage?: ThreadResumeInitialTurnsPageParams | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadResumeResponse.ts b/src/runtimes/openai/generated/v2/ThreadResumeResponse.ts new file mode 100644 index 0000000..b40faca --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadResumeResponse.ts @@ -0,0 +1,68 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { LegacyAppPathString } from "../LegacyAppPathString"; +import type { MultiAgentMode } from "../MultiAgentMode"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ActivePermissionProfile } from "./ActivePermissionProfile"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxPolicy } from "./SandboxPolicy"; +import type { Thread } from "./Thread"; +import type { TurnsPage } from "./TurnsPage"; + +export type ThreadResumeResponse = { + thread: Thread; + model: string; + modelProvider: string; + serviceTier: string | null; + cwd: AbsolutePathBuf; + /** + * Thread-scoped runtime workspace roots used to materialize + * `:workspace_roots`. + */ + runtimeWorkspaceRoots: Array; + /** + * Environment-native paths to instruction source files currently loaded for this thread. + */ + instructionSources: Array; + approvalPolicy: AskForApproval; + /** + * Reviewer currently used for approval requests on this thread. + */ + approvalsReviewer: ApprovalsReviewer; + /** + * Legacy sandbox policy retained for compatibility. Experimental clients + * should prefer `activePermissionProfile` for profile provenance. + */ + sandbox: SandboxPolicy; + /** + * Named or implicit built-in profile that produced the active + * permissions, when known. + */ + activePermissionProfile: ActivePermissionProfile | null; + reasoningEffort: ReasoningEffort | null; + /** + * @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. + */ + multiAgentMode: MultiAgentMode; + /** + * `thread/turns/list` page returned when requested by `initialTurnsPage`. + */ + initialTurnsPage: TurnsPage | null; + /** + * Opaque cursor for hydrating paginated turns backwards. + * + * Pass this as `cursor` to `thread/turns/list` with + * `sortDirection: "desc"`. The first page includes the turn identified by the cursor. + */ + turnsBackwardsCursor: string | null; + /** + * Opaque cursor for hydrating paginated items backwards. + * + * Pass this as `cursor` to `thread/items/list` with + * `sortDirection: "desc"`. The first page includes the item identified by the cursor. + */ + itemsBackwardsCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRevertParams.ts b/src/runtimes/openai/generated/v2/ThreadRevertParams.ts new file mode 100644 index 0000000..8bf114a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRevertParams.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Replace a paginated thread's durable history with the prefix before one turn. + * + * This only changes persisted conversation history. It does not revert local file changes. + */ +export type ThreadRevertParams = { + threadId: string; + /** + * Turn excluded from the replacement history, together with every later turn. + */ + beforeTurnId: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRevertResponse.ts b/src/runtimes/openai/generated/v2/ThreadRevertResponse.ts new file mode 100644 index 0000000..b563369 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRevertResponse.ts @@ -0,0 +1,26 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Thread } from "./Thread"; + +export type ThreadRevertResponse = { + /** + * Updated loaded thread metadata. `turns` is always empty; hydrate retained history through + * `thread/turns/list`. + */ + thread: Thread; + /** + * Opaque cursor for hydrating paginated turns backwards. + * + * Pass this as `cursor` to `thread/turns/list` with + * `sortDirection: "desc"`. The first page includes the turn identified by the cursor. + */ + turnsBackwardsCursor: string | null; + /** + * Opaque cursor for hydrating paginated items backwards. + * + * Pass this as `cursor` to `thread/items/list` with + * `sortDirection: "desc"`. The first page includes the item identified by the cursor. + */ + itemsBackwardsCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRevertedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRevertedNotification.ts new file mode 100644 index 0000000..d36a6fb --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRevertedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadRevertedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRollbackParams.ts b/src/runtimes/openai/generated/v2/ThreadRollbackParams.ts new file mode 100644 index 0000000..d194181 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRollbackParams.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * DEPRECATED: `thread/rollback` will be removed soon. + */ +export type ThreadRollbackParams = { + threadId: string; + /** + * The number of turns to drop from the end of the thread. Must be >= 1. + * + * This only modifies the thread's history and does not revert local file changes + * that have been made by the agent. Clients are responsible for reverting these changes. + */ + numTurns: number; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadRollbackResponse.ts b/src/runtimes/openai/generated/v2/ThreadRollbackResponse.ts new file mode 100644 index 0000000..82f8de4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadRollbackResponse.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Thread } from "./Thread"; + +export type ThreadRollbackResponse = { + /** + * The updated thread after applying the rollback, with `turns` populated. + * + * The ThreadItems stored in each Turn are lossy since we explicitly do not + * persist all agent interactions, such as command executions. This is the same + * behavior as `thread/resume`. + */ + thread: Thread; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchOccurrence.ts b/src/runtimes/openai/generated/v2/ThreadSearchOccurrence.ts new file mode 100644 index 0000000..0a8c82f --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSearchOccurrence.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSearchTextRange } from "./ThreadSearchTextRange"; + +/** + * One visible message occurrence returned by [`ThreadSearchOccurrencesResponse`]. + */ +export type ThreadSearchOccurrence = { + turnId: string; + itemId: string; + snippet: string; + /** + * Match range within `snippet`, in UTF-16 code units. + */ + snippetMatchRange: ThreadSearchTextRange; + /** + * Opaque inclusive cursor accepted by `thread/turns/list` for this turn. + */ + turnCursor: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesParams.ts b/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesParams.ts new file mode 100644 index 0000000..4020e8b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesParams.ts @@ -0,0 +1,23 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Parameters for searching visible message occurrences within one paginated thread. + */ +export type ThreadSearchOccurrencesParams = { + threadId: string; + /** + * Case-insensitive literal substring to find in visible user messages and final assistant + * messages. + */ + searchTerm: string; + /** + * Opaque cursor returned by a previous call for the same thread and search term. + */ + cursor?: string | null; + /** + * Optional occurrence page size. + */ + limit?: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesResponse.ts b/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesResponse.ts new file mode 100644 index 0000000..93cc0ec --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesResponse.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSearchOccurrence } from "./ThreadSearchOccurrence"; + +export type ThreadSearchOccurrencesResponse = { + /** + * Occurrences in chronological message order. + */ + data: Array; + /** + * Opaque cursor to continue after the last returned occurrence. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchParams.ts b/src/runtimes/openai/generated/v2/ThreadSearchParams.ts new file mode 100644 index 0000000..b3b8073 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSearchParams.ts @@ -0,0 +1,39 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SortDirection } from "./SortDirection"; +import type { ThreadSearchSortKey } from "./ThreadSearchSortKey"; +import type { ThreadSourceKind } from "./ThreadSourceKind"; + +export type ThreadSearchParams = { + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Optional page size; defaults to a reasonable server-side value. + */ + limit?: number | null; + /** + * Optional sort key; defaults to created_at. + */ + sortKey?: ThreadSearchSortKey | null; + /** + * Optional sort direction; defaults to descending (newest first). + */ + sortDirection?: SortDirection | null; + /** + * Optional source filter; when set, only sessions from these source kinds + * are returned. When omitted or empty, defaults to interactive sources. + */ + sourceKinds?: Array | null; + /** + * Optional archived filter; when set to true, only archived threads are returned. + * If false or null, only non-archived threads are returned. + */ + archived?: boolean | null; + /** + * Required substring/full-text query for thread search. + */ + searchTerm: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchResponse.ts b/src/runtimes/openai/generated/v2/ThreadSearchResponse.ts new file mode 100644 index 0000000..5353aad --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSearchResponse.ts @@ -0,0 +1,20 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSearchResult } from "./ThreadSearchResult"; + +export type ThreadSearchResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last item. + * if None, there are no more items to return. + */ + nextCursor: string | null; + /** + * Opaque cursor to pass as `cursor` when reversing `sortDirection`. + * This is only populated when the page contains at least one thread. + * Use it with the opposite `sortDirection`; for timestamp sorts it anchors + * at the start of the page timestamp so same-second updates are not skipped. + */ + backwardsCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchResult.ts b/src/runtimes/openai/generated/v2/ThreadSearchResult.ts new file mode 100644 index 0000000..84efaa0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSearchResult.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Thread } from "./Thread"; + +export type ThreadSearchResult = { thread: Thread; snippet: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchSortKey.ts b/src/runtimes/openai/generated/v2/ThreadSearchSortKey.ts new file mode 100644 index 0000000..4abf271 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSearchSortKey.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadSearchSortKey = "created_at" | "updated_at" | "recency_at"; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchTextRange.ts b/src/runtimes/openai/generated/v2/ThreadSearchTextRange.ts new file mode 100644 index 0000000..0545e13 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSearchTextRange.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * UTF-16 code-unit range within `snippet`. + */ +export type ThreadSearchTextRange = { + /** + * Inclusive UTF-16 code-unit offset. + */ + start: number; + /** + * Exclusive UTF-16 code-unit offset. + */ + end: number; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSection.ts b/src/runtimes/openai/generated/v2/ThreadSection.ts new file mode 100644 index 0000000..77121cf --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSection.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; + +/** + * An independently persisted, user-visible thread section. + */ +export type ThreadSection = { + /** + * Opaque UUIDv7 identity that remains stable when the section is renamed. + */ + id: string; + /** + * The current user-visible section name. + */ + name: string; + /** + * Optional appearance synchronized across clients. + */ + appearance: ThreadSectionAppearance | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionAppearance.ts b/src/runtimes/openai/generated/v2/ThreadSectionAppearance.ts new file mode 100644 index 0000000..f99774c --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionAppearance.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Extensible visual presentation for a custom thread section. + */ +export type ThreadSectionAppearance = { icon: string | null; color: string | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionCreateParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionCreateParams.ts new file mode 100644 index 0000000..929fd5a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionCreateParams.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; + +/** + * Parameters for creating an independently persisted thread section. + */ +export type ThreadSectionCreateParams = { + /** + * The user-visible name of the section. + */ + name: string; + appearance?: ThreadSectionAppearance | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionCreateResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionCreateResponse.ts new file mode 100644 index 0000000..8a1b362 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionCreateResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSection } from "./ThreadSection"; + +/** + * The independently persisted section created by the server. + */ +export type ThreadSectionCreateResponse = { section: ThreadSection }; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionDeleteParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionDeleteParams.ts new file mode 100644 index 0000000..425eb48 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionDeleteParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Parameters for deleting an independently persisted thread section. + */ +export type ThreadSectionDeleteParams = { + /** + * The stable, server-generated identity of the section to delete. + */ + sectionId: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionDeleteResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionDeleteResponse.ts new file mode 100644 index 0000000..0ad58d0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionDeleteResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful deletion does not return additional section data. + */ +export type ThreadSectionDeleteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionListParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionListParams.ts new file mode 100644 index 0000000..abf8bd3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionListParams.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Parameters for listing independently persisted thread sections. + */ +export type ThreadSectionListParams = { + /** + * Opaque pagination cursor returned by a previous call. + */ + cursor?: string | null; + /** + * Maximum number of sections to return. + */ + limit?: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionListResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionListResponse.ts new file mode 100644 index 0000000..b3723a7 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionListResponse.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSection } from "./ThreadSection"; + +/** + * One page of independently persisted thread sections. + */ +export type ThreadSectionListResponse = { + data: Array; + /** + * Opaque cursor for the next page, or `null` when no sections remain. + */ + nextCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionMoveParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionMoveParams.ts new file mode 100644 index 0000000..b8795f3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionMoveParams.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Parameters for moving a thread within a server-owned section ordering. + */ +export type ThreadSectionMoveParams = { + /** + * Thread to move into, within, or out of a section. + */ + threadId: string; + /** + * Destination section, or `null` to remove the thread from its section. + */ + sectionId: string | null; + /** + * Existing thread to insert before; omission or null appends to the section. + */ + beforeThreadId?: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionMoveResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionMoveResponse.ts new file mode 100644 index 0000000..e9e0f43 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionMoveResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadSectionMoveResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionUpdateParams.ts new file mode 100644 index 0000000..c7c354a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionUpdateParams.ts @@ -0,0 +1,22 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; + +/** + * Parameters for updating an independently persisted thread section. + */ +export type ThreadSectionUpdateParams = { + /** + * The stable, server-generated identity of the section to update. + */ + sectionId: string; + /** + * The updated user-visible name of the section. + */ + name: string; + /** + * Omit to preserve appearance, use `null` to clear it, or provide a replacement. + */ + appearance?: ThreadSectionAppearance | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionUpdateResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionUpdateResponse.ts new file mode 100644 index 0000000..aef9241 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSectionUpdateResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSection } from "./ThreadSection"; + +/** + * The independently persisted section after its name is updated. + */ +export type ThreadSectionUpdateResponse = { section: ThreadSection }; diff --git a/src/runtimes/openai/generated/v2/ThreadSetNameParams.ts b/src/runtimes/openai/generated/v2/ThreadSetNameParams.ts new file mode 100644 index 0000000..17d12ee --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSetNameParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadSetNameParams = { threadId: string; name: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadSetNameResponse.ts b/src/runtimes/openai/generated/v2/ThreadSetNameResponse.ts new file mode 100644 index 0000000..09143d2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSetNameResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadSetNameResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSettings.ts b/src/runtimes/openai/generated/v2/ThreadSettings.ts new file mode 100644 index 0000000..b2c2863 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSettings.ts @@ -0,0 +1,32 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { CollaborationMode } from "../CollaborationMode"; +import type { MultiAgentMode } from "../MultiAgentMode"; +import type { Personality } from "../Personality"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ReasoningSummary } from "../ReasoningSummary"; +import type { ActivePermissionProfile } from "./ActivePermissionProfile"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxPolicy } from "./SandboxPolicy"; + +export type ThreadSettings = { + cwd: AbsolutePathBuf; + approvalPolicy: AskForApproval; + approvalsReviewer: ApprovalsReviewer; + sandboxPolicy: SandboxPolicy; + activePermissionProfile: ActivePermissionProfile | null; + model: string; + modelProvider: string; + serviceTier: string | null; + effort: ReasoningEffort | null; + summary: ReasoningSummary | null; + collaborationMode: CollaborationMode; + /** + * @deprecated Always `explicitRequestOnly`. Use `effort` for Ultra behavior. + */ + multiAgentMode: MultiAgentMode; + personality: Personality | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSettingsUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadSettingsUpdateParams.ts new file mode 100644 index 0000000..1b64fa1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSettingsUpdateParams.ts @@ -0,0 +1,68 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CollaborationMode } from "../CollaborationMode"; +import type { MultiAgentMode } from "../MultiAgentMode"; +import type { Personality } from "../Personality"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ReasoningSummary } from "../ReasoningSummary"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxPolicy } from "./SandboxPolicy"; + +export type ThreadSettingsUpdateParams = { + threadId: string; + /** + * Override the working directory for subsequent turns. + */ + cwd?: string | null; + /** + * Override the approval policy for subsequent turns. + */ + approvalPolicy?: AskForApproval | null; + /** + * Override where approval requests are routed for subsequent turns. + */ + approvalsReviewer?: ApprovalsReviewer | null; + /** + * Override the sandbox policy for subsequent turns. + */ + sandboxPolicy?: SandboxPolicy | null; + /** + * Select a named permissions profile id for subsequent turns. Cannot be + * combined with `sandboxPolicy`. + */ + permissions?: string | null; + /** + * Override the model for subsequent turns. + */ + model?: string | null; + /** + * Override the service tier for subsequent turns. `null` clears the + * current service tier; omission leaves it unchanged. + */ + serviceTier?: string | null | null; + /** + * Override the reasoning effort for subsequent turns. + */ + effort?: ReasoningEffort | null; + /** + * Override the reasoning summary for subsequent turns. + */ + summary?: ReasoningSummary | null; + /** + * EXPERIMENTAL - Set a pre-set collaboration mode for subsequent turns. + * + * For `collaboration_mode.settings.developer_instructions`, `null` means + * "use the built-in instructions for the selected mode". + */ + collaborationMode?: CollaborationMode | null; + /** + * @deprecated Ignored. Use `effort: "ultra"` for proactive multi-agent behavior. + */ + multiAgentMode?: MultiAgentMode | null; + /** + * Override the personality for subsequent turns. + */ + personality?: Personality | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadSettingsUpdateResponse.ts b/src/runtimes/openai/generated/v2/ThreadSettingsUpdateResponse.ts new file mode 100644 index 0000000..06afb97 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSettingsUpdateResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadSettingsUpdateResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSettingsUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadSettingsUpdatedNotification.ts new file mode 100644 index 0000000..fc911c1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSettingsUpdatedNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSettings } from "./ThreadSettings"; + +export type ThreadSettingsUpdatedNotification = { + threadId: string; + threadSettings: ThreadSettings; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadShellCommandParams.ts b/src/runtimes/openai/generated/v2/ThreadShellCommandParams.ts new file mode 100644 index 0000000..7e7b118 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadShellCommandParams.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadShellCommandParams = { + threadId: string; + /** + * Shell command string evaluated by the thread's configured shell. + * Unlike `command/exec`, this intentionally preserves shell syntax + * such as pipes, redirects, and quoting. This runs unsandboxed with full + * access rather than inheriting the thread sandbox policy. + */ + command: string; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadShellCommandResponse.ts b/src/runtimes/openai/generated/v2/ThreadShellCommandResponse.ts new file mode 100644 index 0000000..9c54b45 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadShellCommandResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadShellCommandResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSortKey.ts b/src/runtimes/openai/generated/v2/ThreadSortKey.ts new file mode 100644 index 0000000..21eae4e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSortKey.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadSortKey = "created_at" | "updated_at" | "recency_at" | "section_position"; diff --git a/src/runtimes/openai/generated/v2/ThreadSource.ts b/src/runtimes/openai/generated/v2/ThreadSource.ts new file mode 100644 index 0000000..f27154a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSource.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadSource = string; diff --git a/src/runtimes/openai/generated/v2/ThreadSourceKind.ts b/src/runtimes/openai/generated/v2/ThreadSourceKind.ts new file mode 100644 index 0000000..39b1887 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadSourceKind.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadSourceKind = + | "cli" + | "vscode" + | "exec" + | "appServer" + | "subAgent" + | "subAgentReview" + | "subAgentCompact" + | "subAgentThreadSpawn" + | "subAgentOther" + | "unknown"; diff --git a/src/runtimes/openai/generated/v2/ThreadStartParams.ts b/src/runtimes/openai/generated/v2/ThreadStartParams.ts new file mode 100644 index 0000000..35745ab --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadStartParams.ts @@ -0,0 +1,91 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { MultiAgentMode } from "../MultiAgentMode"; +import type { Personality } from "../Personality"; +import type { JsonValue } from "../serde_json/JsonValue"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { DynamicToolSpec } from "./DynamicToolSpec"; +import type { SandboxMode } from "./SandboxMode"; +import type { SelectedCapabilityRoot } from "./SelectedCapabilityRoot"; +import type { ThreadHistoryMode } from "./ThreadHistoryMode"; +import type { ThreadSource } from "./ThreadSource"; +import type { ThreadStartSource } from "./ThreadStartSource"; +import type { TurnEnvironmentParams } from "./TurnEnvironmentParams"; + +export type ThreadStartParams = { + model?: string | null; + modelProvider?: string | null; + /** + * Allow a provider with an authoritative static model catalog to replace an unavailable + * requested model with its default. + */ + allowProviderModelFallback?: boolean; + serviceTier?: string | null | null; + cwd?: string | null; + /** + * Replace the thread's runtime workspace roots. Paths must be absolute. + */ + runtimeWorkspaceRoots?: Array | null; + approvalPolicy?: AskForApproval | null; + /** + * Override where approval requests are routed for review on this thread + * and subsequent turns. + */ + approvalsReviewer?: ApprovalsReviewer | null; + sandbox?: SandboxMode | null; + /** + * Named profile id for this thread. Cannot be combined with `sandbox`. + */ + permissions?: string | null; + config?: { [key in string]?: JsonValue } | null; + serviceName?: string | null; + baseInstructions?: string | null; + developerInstructions?: string | null; + personality?: Personality | null; + /** + * @deprecated Ignored. Use Ultra reasoning effort for proactive multi-agent behavior. + */ + multiAgentMode?: MultiAgentMode | null; + ephemeral?: boolean | null; + /** + * Persisted thread history contract to use for this new thread. + */ + historyMode?: ThreadHistoryMode | null; + sessionStartSource?: ThreadStartSource | null; + /** + * Optional client-supplied analytics source classification for this thread. + */ + threadSource?: ThreadSource | null; + /** + * Optional project identity for this new thread. Durable threads persist + * the assignment; ephemeral threads expose it only in live responses. + */ + projectId?: string | null; + /** + * Optional sticky environments for this thread. + * + * Omitted selects the default environment when environment access is + * enabled. Empty disables environment access for turns that do not + * provide a turn override. Non-empty selects the first environment as the + * current turn environment. + */ + environments?: Array | null; + dynamicTools?: Array | null; + /** + * Capability roots selected for this thread by the hosting platform. + */ + selectedCapabilityRoots?: Array | null; + /** + * Test-only experimental field used to validate experimental gating and + * schema filtering behavior in a stable way. + */ + mockExperimentalField?: string | null; + /** + * If true, opt into emitting raw Responses API items on the event stream. + * This is for internal use only (e.g. Codex Cloud). + */ + experimentalRawEvents?: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadStartResponse.ts b/src/runtimes/openai/generated/v2/ThreadStartResponse.ts new file mode 100644 index 0000000..0b616e2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadStartResponse.ts @@ -0,0 +1,49 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { LegacyAppPathString } from "../LegacyAppPathString"; +import type { MultiAgentMode } from "../MultiAgentMode"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ActivePermissionProfile } from "./ActivePermissionProfile"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxPolicy } from "./SandboxPolicy"; +import type { Thread } from "./Thread"; + +export type ThreadStartResponse = { + thread: Thread; + model: string; + modelProvider: string; + serviceTier: string | null; + cwd: AbsolutePathBuf; + /** + * Thread-scoped runtime workspace roots used to materialize + * `:workspace_roots`. + */ + runtimeWorkspaceRoots: Array; + /** + * Environment-native paths to instruction source files currently loaded for this thread. + */ + instructionSources: Array; + approvalPolicy: AskForApproval; + /** + * Reviewer currently used for approval requests on this thread. + */ + approvalsReviewer: ApprovalsReviewer; + /** + * Legacy sandbox policy retained for compatibility. Experimental clients + * should prefer `activePermissionProfile` for profile provenance. + */ + sandbox: SandboxPolicy; + /** + * Named or implicit built-in profile that produced the active + * permissions, when known. + */ + activePermissionProfile: ActivePermissionProfile | null; + reasoningEffort: ReasoningEffort | null; + /** + * @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. + */ + multiAgentMode: MultiAgentMode; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadStartSource.ts b/src/runtimes/openai/generated/v2/ThreadStartSource.ts new file mode 100644 index 0000000..ea1b839 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadStartSource.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadStartSource = "startup" | "clear"; diff --git a/src/runtimes/openai/generated/v2/ThreadStartedNotification.ts b/src/runtimes/openai/generated/v2/ThreadStartedNotification.ts new file mode 100644 index 0000000..82fce9e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadStartedNotification.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Thread } from "./Thread"; + +export type ThreadStartedNotification = { thread: Thread }; diff --git a/src/runtimes/openai/generated/v2/ThreadStatus.ts b/src/runtimes/openai/generated/v2/ThreadStatus.ts new file mode 100644 index 0000000..64059b6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadStatus.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadActiveFlag } from "./ThreadActiveFlag"; + +export type ThreadStatus = + | { type: "notLoaded" } + | { type: "idle" } + | { type: "systemError" } + | { type: "active"; activeFlags: Array }; diff --git a/src/runtimes/openai/generated/v2/ThreadStatusChangedNotification.ts b/src/runtimes/openai/generated/v2/ThreadStatusChangedNotification.ts new file mode 100644 index 0000000..3fc2e9a --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadStatusChangedNotification.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadStatus } from "./ThreadStatus"; + +export type ThreadStatusChangedNotification = { threadId: string; status: ThreadStatus }; diff --git a/src/runtimes/openai/generated/v2/ThreadTimelineEntry.ts b/src/runtimes/openai/generated/v2/ThreadTimelineEntry.ts new file mode 100644 index 0000000..27d2915 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadTimelineEntry.ts @@ -0,0 +1,25 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadItem } from "./ThreadItem"; +import type { ThreadRealtimeItem } from "./ThreadRealtimeItem"; +import type { TurnError } from "./TurnError"; +import type { TurnStatus } from "./TurnStatus"; + +/** + * EXPERIMENTAL - one item or turn boundary in canonical rollout order. + */ +export type ThreadTimelineEntry = + | { type: "item"; position: number; turnId: string; item: ThreadItem } + | { type: "realtime"; position: number; item: ThreadRealtimeItem } + | { type: "turnStarted"; position: number; turnId: string; startedAt: number | null } + | { + type: "turnCompleted"; + position: number; + turnId: string; + status: TurnStatus; + error: TurnError | null; + startedAt: number | null; + completedAt: number | null; + durationMs: number | null; + }; diff --git a/src/runtimes/openai/generated/v2/ThreadTimelineListParams.ts b/src/runtimes/openai/generated/v2/ThreadTimelineListParams.ts new file mode 100644 index 0000000..0d1d2fe --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadTimelineListParams.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - list ordinary and realtime thread history in rollout order. + */ +export type ThreadTimelineListParams = { + threadId: string; + cursor?: string | null; + limit?: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadTimelineListResponse.ts b/src/runtimes/openai/generated/v2/ThreadTimelineListResponse.ts new file mode 100644 index 0000000..adc0195 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadTimelineListResponse.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadTimelineEntry } from "./ThreadTimelineEntry"; + +/** + * EXPERIMENTAL - a bounded timeline page with its resolved opening voice state. + */ +export type ThreadTimelineListResponse = { + data: Array; + nextCursor: string | null; + activeRealtimeSessionAtPageStart: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadTokenUsage.ts b/src/runtimes/openai/generated/v2/ThreadTokenUsage.ts new file mode 100644 index 0000000..525619b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadTokenUsage.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; + +export type ThreadTokenUsage = { + total: TokenUsageBreakdown; + last: TokenUsageBreakdown; + modelContextWindow: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadTokenUsageUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadTokenUsageUpdatedNotification.ts new file mode 100644 index 0000000..59ce27f --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadTokenUsageUpdatedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadTokenUsage } from "./ThreadTokenUsage"; + +export type ThreadTokenUsageUpdatedNotification = { + threadId: string; + turnId: string; + tokenUsage: ThreadTokenUsage; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadTurnsListParams.ts b/src/runtimes/openai/generated/v2/ThreadTurnsListParams.ts new file mode 100644 index 0000000..77b8163 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadTurnsListParams.ts @@ -0,0 +1,25 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SortDirection } from "./SortDirection"; +import type { TurnItemsView } from "./TurnItemsView"; + +export type ThreadTurnsListParams = { + threadId: string; + /** + * Opaque cursor to pass to the next call to continue after the last turn. + */ + cursor?: string | null; + /** + * Optional turn page size. + */ + limit?: number | null; + /** + * Optional turn pagination direction; defaults to descending. + */ + sortDirection?: SortDirection | null; + /** + * How much item detail to include for each returned turn; defaults to summary. + */ + itemsView?: TurnItemsView | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadTurnsListResponse.ts b/src/runtimes/openai/generated/v2/ThreadTurnsListResponse.ts new file mode 100644 index 0000000..9c961dd --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadTurnsListResponse.ts @@ -0,0 +1,20 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Turn } from "./Turn"; + +export type ThreadTurnsListResponse = { + data: Array; + /** + * Opaque cursor to pass to the next call to continue after the last turn. + * if None, there are no more turns to return. + */ + nextCursor: string | null; + /** + * Opaque cursor to pass as `cursor` when reversing `sortDirection`. + * This is only populated when the page contains at least one turn. + * Use it with the opposite `sortDirection` to include the anchor turn again + * and catch updates to that turn. + */ + backwardsCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadUnarchiveParams.ts b/src/runtimes/openai/generated/v2/ThreadUnarchiveParams.ts new file mode 100644 index 0000000..9b01abc --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadUnarchiveParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadUnarchiveParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnarchiveResponse.ts b/src/runtimes/openai/generated/v2/ThreadUnarchiveResponse.ts new file mode 100644 index 0000000..a469265 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadUnarchiveResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Thread } from "./Thread"; + +export type ThreadUnarchiveResponse = { thread: Thread }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnarchivedNotification.ts b/src/runtimes/openai/generated/v2/ThreadUnarchivedNotification.ts new file mode 100644 index 0000000..34a712e --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadUnarchivedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadUnarchivedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnsubscribeParams.ts b/src/runtimes/openai/generated/v2/ThreadUnsubscribeParams.ts new file mode 100644 index 0000000..0fd5aae --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadUnsubscribeParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadUnsubscribeParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnsubscribeResponse.ts b/src/runtimes/openai/generated/v2/ThreadUnsubscribeResponse.ts new file mode 100644 index 0000000..10db583 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadUnsubscribeResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus"; + +export type ThreadUnsubscribeResponse = { status: ThreadUnsubscribeStatus }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnsubscribeStatus.ts b/src/runtimes/openai/generated/v2/ThreadUnsubscribeStatus.ts new file mode 100644 index 0000000..2970598 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadUnsubscribeStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadUnsubscribeStatus = "notLoaded" | "notSubscribed" | "unsubscribed"; diff --git a/src/runtimes/openai/generated/v2/ThreadUsage.ts b/src/runtimes/openai/generated/v2/ThreadUsage.ts new file mode 100644 index 0000000..14a00ca --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadUsage.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadUsageBreakdownGroup } from "./ThreadUsageBreakdownGroup"; + +export type ThreadUsage = { + threadId: string; + estimatedUsageCreditsMicros: bigint; + estimatedUsageUsdMicros: bigint | null; + groups: Array; +}; diff --git a/src/runtimes/openai/generated/v2/ThreadUsageBreakdownGroup.ts b/src/runtimes/openai/generated/v2/ThreadUsageBreakdownGroup.ts new file mode 100644 index 0000000..886ee8b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ThreadUsageBreakdownGroup.ts @@ -0,0 +1,15 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadUsageBreakdownGroup = { + model: string | null; + reasoningEffort: string | null; + speed: string | null; + estimatedUsageCreditsMicros: bigint; + netNewInputTokens: bigint | null; + cachedInputTokens: bigint | null; + inputTokens: bigint | null; + outputTokens: bigint | null; + totalTokens: bigint | null; +}; diff --git a/src/runtimes/openai/generated/v2/TokenUsageBreakdown.ts b/src/runtimes/openai/generated/v2/TokenUsageBreakdown.ts new file mode 100644 index 0000000..4837e56 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TokenUsageBreakdown.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TokenUsageBreakdown = { + totalTokens: number; + inputTokens: number; + cachedInputTokens: number; + cacheWriteInputTokens: number; + outputTokens: number; + reasoningOutputTokens: number; +}; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputAnswer.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputAnswer.ts new file mode 100644 index 0000000..87e581b --- /dev/null +++ b/src/runtimes/openai/generated/v2/ToolRequestUserInputAnswer.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL. Captures a user's answer to a request_user_input question. + */ +export type ToolRequestUserInputAnswer = { answers: Array }; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputOption.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputOption.ts new file mode 100644 index 0000000..6a92bc0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ToolRequestUserInputOption.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL. Defines a single selectable option for request_user_input. + */ +export type ToolRequestUserInputOption = { label: string; description: string }; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputParams.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputParams.ts new file mode 100644 index 0000000..bb5d593 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ToolRequestUserInputParams.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion"; + +/** + * EXPERIMENTAL. Params sent with a request_user_input event. + */ +export type ToolRequestUserInputParams = { + threadId: string; + turnId: string; + itemId: string; + questions: Array; + isBlocking: boolean; + /** + * @deprecated Use `isBlocking` to decide whether the request should block. + */ + autoResolutionMs: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputQuestion.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputQuestion.ts new file mode 100644 index 0000000..cc1d12d --- /dev/null +++ b/src/runtimes/openai/generated/v2/ToolRequestUserInputQuestion.ts @@ -0,0 +1,16 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption"; + +/** + * EXPERIMENTAL. Represents one request_user_input question and its required options. + */ +export type ToolRequestUserInputQuestion = { + id: string; + header: string; + question: string; + isOther: boolean; + isSecret: boolean; + options: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputResponse.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputResponse.ts new file mode 100644 index 0000000..6005100 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ToolRequestUserInputResponse.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer"; + +/** + * EXPERIMENTAL. Response payload mapping question ids to answers. + */ +export type ToolRequestUserInputResponse = { + answers: { [key in string]?: ToolRequestUserInputAnswer }; +}; diff --git a/src/runtimes/openai/generated/v2/ToolsV2.ts b/src/runtimes/openai/generated/v2/ToolsV2.ts new file mode 100644 index 0000000..5a563e3 --- /dev/null +++ b/src/runtimes/openai/generated/v2/ToolsV2.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WebSearchToolConfig } from "../WebSearchToolConfig"; + +export type ToolsV2 = { web_search: WebSearchToolConfig | null }; diff --git a/src/runtimes/openai/generated/v2/Turn.ts b/src/runtimes/openai/generated/v2/Turn.ts new file mode 100644 index 0000000..92501a8 --- /dev/null +++ b/src/runtimes/openai/generated/v2/Turn.ts @@ -0,0 +1,39 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadItem } from "./ThreadItem"; +import type { TurnError } from "./TurnError"; +import type { TurnItemsView } from "./TurnItemsView"; +import type { TurnStatus } from "./TurnStatus"; + +export type Turn = { + /** + * Identifier for this turn. Codex-generated turn IDs are UUIDv7. + */ + id: string; + /** + * Thread items currently included in this turn payload. + */ + items: Array; + /** + * Describes how much of `items` has been loaded for this turn. + */ + itemsView: TurnItemsView; + status: TurnStatus; + /** + * Only populated when the Turn's status is failed. + */ + error: TurnError | null; + /** + * Unix timestamp (in seconds) when the turn started. + */ + startedAt: number | null; + /** + * Unix timestamp (in seconds) when the turn completed. + */ + completedAt: number | null; + /** + * Duration between turn start and completion in milliseconds, if known. + */ + durationMs: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/TurnCompletedNotification.ts b/src/runtimes/openai/generated/v2/TurnCompletedNotification.ts new file mode 100644 index 0000000..5b585e0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnCompletedNotification.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Turn } from "./Turn"; + +export type TurnCompletedNotification = { threadId: string; turn: Turn }; diff --git a/src/runtimes/openai/generated/v2/TurnDiffUpdatedNotification.ts b/src/runtimes/openai/generated/v2/TurnDiffUpdatedNotification.ts new file mode 100644 index 0000000..0f4bf14 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnDiffUpdatedNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Notification that the turn-level unified diff has changed. + * Contains the latest aggregated diff across all file changes in the turn. + */ +export type TurnDiffUpdatedNotification = { threadId: string; turnId: string; diff: string }; diff --git a/src/runtimes/openai/generated/v2/TurnEnvironmentParams.ts b/src/runtimes/openai/generated/v2/TurnEnvironmentParams.ts new file mode 100644 index 0000000..566cba2 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnEnvironmentParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LegacyAppPathString } from "../LegacyAppPathString"; + +export type TurnEnvironmentParams = { + environmentId: string; + cwd: LegacyAppPathString; + /** + * Environment-native runtime workspace roots. Omitted defaults to `cwd`. + */ + runtimeWorkspaceRoots?: Array | null; +}; diff --git a/src/runtimes/openai/generated/v2/TurnError.ts b/src/runtimes/openai/generated/v2/TurnError.ts new file mode 100644 index 0000000..8b5aee1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnError.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CodexErrorInfo } from "./CodexErrorInfo"; + +export type TurnError = { + message: string; + codexErrorInfo: CodexErrorInfo | null; + additionalDetails: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/TurnInterruptParams.ts b/src/runtimes/openai/generated/v2/TurnInterruptParams.ts new file mode 100644 index 0000000..1373415 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnInterruptParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TurnInterruptParams = { threadId: string; turnId: string }; diff --git a/src/runtimes/openai/generated/v2/TurnInterruptResponse.ts b/src/runtimes/openai/generated/v2/TurnInterruptResponse.ts new file mode 100644 index 0000000..7ce6e35 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnInterruptResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TurnInterruptResponse = Record; diff --git a/src/runtimes/openai/generated/v2/TurnItemsView.ts b/src/runtimes/openai/generated/v2/TurnItemsView.ts new file mode 100644 index 0000000..9056923 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnItemsView.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TurnItemsView = "notLoaded" | "summary" | "full"; diff --git a/src/runtimes/openai/generated/v2/TurnModerationMetadataNotification.ts b/src/runtimes/openai/generated/v2/TurnModerationMetadataNotification.ts new file mode 100644 index 0000000..0d9f07e --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnModerationMetadataNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type TurnModerationMetadataNotification = { + threadId: string; + turnId: string; + metadata: JsonValue; +}; diff --git a/src/runtimes/openai/generated/v2/TurnPlanStep.ts b/src/runtimes/openai/generated/v2/TurnPlanStep.ts new file mode 100644 index 0000000..7e3c020 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnPlanStep.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TurnPlanStepStatus } from "./TurnPlanStepStatus"; + +export type TurnPlanStep = { step: string; status: TurnPlanStepStatus }; diff --git a/src/runtimes/openai/generated/v2/TurnPlanStepStatus.ts b/src/runtimes/openai/generated/v2/TurnPlanStepStatus.ts new file mode 100644 index 0000000..f6733a6 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnPlanStepStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TurnPlanStepStatus = "pending" | "inProgress" | "completed"; diff --git a/src/runtimes/openai/generated/v2/TurnPlanUpdatedNotification.ts b/src/runtimes/openai/generated/v2/TurnPlanUpdatedNotification.ts new file mode 100644 index 0000000..6e48f19 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnPlanUpdatedNotification.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TurnPlanStep } from "./TurnPlanStep"; + +export type TurnPlanUpdatedNotification = { + threadId: string; + turnId: string; + explanation: string | null; + plan: Array; +}; diff --git a/src/runtimes/openai/generated/v2/TurnStartParams.ts b/src/runtimes/openai/generated/v2/TurnStartParams.ts new file mode 100644 index 0000000..17b8327 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnStartParams.ts @@ -0,0 +1,108 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { CollaborationMode } from "../CollaborationMode"; +import type { MultiAgentMode } from "../MultiAgentMode"; +import type { Personality } from "../Personality"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ReasoningSummary } from "../ReasoningSummary"; +import type { JsonValue } from "../serde_json/JsonValue"; +import type { AdditionalContextEntry } from "./AdditionalContextEntry"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxPolicy } from "./SandboxPolicy"; +import type { TurnEnvironmentParams } from "./TurnEnvironmentParams"; +import type { UserInput } from "./UserInput"; + +export type TurnStartParams = { + threadId: string; + clientUserMessageId?: string | null; + input: Array; + /** + * Optional metadata to enrich Codex's ResponsesAPI turn metadata. + * + * Entries are flattened into the JSON string sent as + * `client_metadata["x-codex-turn-metadata"]` on ResponsesAPI HTTP and websocket requests. + * + * They are not sent as top-level ResponsesAPI `client_metadata` keys, and reserved keys + * such as `session_id`, `thread_id`, `turn_id`, and `window_id` cannot be overridden. + */ + responsesapiClientMetadata?: { [key in string]?: string } | null; + /** + * Optional client-provided context fragments keyed by an opaque source identifier. + */ + additionalContext?: { [key in string]?: AdditionalContextEntry } | null; + /** + * Optional environments for this turn and subsequent turns. + * + * Omitted uses the thread sticky environments. Empty disables + * environment access for this turn. Non-empty selects the first + * environment as the current turn environment for this turn. + */ + environments?: Array | null; + /** + * Override the working directory for this turn and subsequent turns. + */ + cwd?: string | null; + /** + * Replace the thread's runtime workspace roots for this turn and + * subsequent turns. Paths must be absolute. + */ + runtimeWorkspaceRoots?: Array | null; + /** + * Override the approval policy for this turn and subsequent turns. + */ + approvalPolicy?: AskForApproval | null; + /** + * Override where approval requests are routed for review on this turn and + * subsequent turns. + */ + approvalsReviewer?: ApprovalsReviewer | null; + /** + * Override the sandbox policy for this turn and subsequent turns. + */ + sandboxPolicy?: SandboxPolicy | null; + /** + * Select a named permissions profile id for this turn and subsequent + * turns. Cannot be combined with `sandboxPolicy`. + */ + permissions?: string | null; + /** + * Override the model for this turn and subsequent turns. + */ + model?: string | null; + /** + * Override the service tier for this turn and subsequent turns. + */ + serviceTier?: string | null | null; + /** + * Override the reasoning effort for this turn and subsequent turns. + */ + effort?: ReasoningEffort | null; + /** + * Override the reasoning summary for this turn and subsequent turns. + */ + summary?: ReasoningSummary | null; + /** + * Override the personality for this turn and subsequent turns. + */ + personality?: Personality | null; + /** + * Optional JSON Schema used to constrain the final assistant message for + * this turn. + */ + outputSchema?: JsonValue | null; + /** + * EXPERIMENTAL - Set a pre-set collaboration mode. + * Takes precedence over model, reasoning_effort, and developer instructions if set. + * + * For `collaboration_mode.settings.developer_instructions`, `null` means + * "use the built-in instructions for the selected mode". + */ + collaborationMode?: CollaborationMode | null; + /** + * @deprecated Ignored. Use `effort: "ultra"` for proactive multi-agent behavior. + */ + multiAgentMode?: MultiAgentMode | null; +}; diff --git a/src/runtimes/openai/generated/v2/TurnStartResponse.ts b/src/runtimes/openai/generated/v2/TurnStartResponse.ts new file mode 100644 index 0000000..d742f41 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnStartResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Turn } from "./Turn"; + +export type TurnStartResponse = { turn: Turn }; diff --git a/src/runtimes/openai/generated/v2/TurnStartedNotification.ts b/src/runtimes/openai/generated/v2/TurnStartedNotification.ts new file mode 100644 index 0000000..0151127 --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnStartedNotification.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Turn } from "./Turn"; + +export type TurnStartedNotification = { threadId: string; turn: Turn }; diff --git a/src/runtimes/openai/generated/v2/TurnStatus.ts b/src/runtimes/openai/generated/v2/TurnStatus.ts new file mode 100644 index 0000000..476922e --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress"; diff --git a/src/runtimes/openai/generated/v2/TurnSteerParams.ts b/src/runtimes/openai/generated/v2/TurnSteerParams.ts new file mode 100644 index 0000000..fddb58c --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnSteerParams.ts @@ -0,0 +1,30 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AdditionalContextEntry } from "./AdditionalContextEntry"; +import type { UserInput } from "./UserInput"; + +export type TurnSteerParams = { + threadId: string; + clientUserMessageId?: string | null; + input: Array; + /** + * Optional metadata to enrich Codex's ResponsesAPI turn metadata. + * + * Entries are flattened into the JSON string sent as + * `client_metadata["x-codex-turn-metadata"]` on ResponsesAPI HTTP and websocket requests. + * + * They are not sent as top-level ResponsesAPI `client_metadata` keys, and reserved keys + * such as `session_id`, `thread_id`, `turn_id`, and `window_id` cannot be overridden. + */ + responsesapiClientMetadata?: { [key in string]?: string } | null; + /** + * Optional client-provided context fragments keyed by an opaque source identifier. + */ + additionalContext?: { [key in string]?: AdditionalContextEntry } | null; + /** + * Required active turn id precondition. The request fails when it does not + * match the currently active turn. + */ + expectedTurnId: string; +}; diff --git a/src/runtimes/openai/generated/v2/TurnSteerResponse.ts b/src/runtimes/openai/generated/v2/TurnSteerResponse.ts new file mode 100644 index 0000000..1ed155c --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnSteerResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TurnSteerResponse = { turnId: string }; diff --git a/src/runtimes/openai/generated/v2/TurnsPage.ts b/src/runtimes/openai/generated/v2/TurnsPage.ts new file mode 100644 index 0000000..978c3fa --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnsPage.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Turn } from "./Turn"; + +export type TurnsPage = { + data: Array; + nextCursor: string | null; + backwardsCursor: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/UserInput.ts b/src/runtimes/openai/generated/v2/UserInput.ts new file mode 100644 index 0000000..f879af0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/UserInput.ts @@ -0,0 +1,21 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ImageDetail } from "../ImageDetail"; +import type { TextElement } from "./TextElement"; + +export type UserInput = + | { + type: "text"; + text: string; + /** + * UI-defined spans within `text` used to render or persist special elements. + */ + text_elements: Array; + } + | { type: "image"; detail?: ImageDetail; url: string } + | { type: "localImage"; detail?: ImageDetail; path: string } + | { type: "audio"; url: string } + | { type: "localAudio"; path: string } + | { type: "skill"; name: string; path: string } + | { type: "mention"; name: string; path: string }; diff --git a/src/runtimes/openai/generated/v2/WarningNotification.ts b/src/runtimes/openai/generated/v2/WarningNotification.ts new file mode 100644 index 0000000..dc30ca0 --- /dev/null +++ b/src/runtimes/openai/generated/v2/WarningNotification.ts @@ -0,0 +1,14 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WarningNotification = { + /** + * Optional thread target when the warning applies to a specific thread. + */ + threadId: string | null; + /** + * Concise warning message for the user. + */ + message: string; +}; diff --git a/src/runtimes/openai/generated/v2/WebSearchAction.ts b/src/runtimes/openai/generated/v2/WebSearchAction.ts new file mode 100644 index 0000000..20801fb --- /dev/null +++ b/src/runtimes/openai/generated/v2/WebSearchAction.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WebSearchAction = + | { type: "search"; query: string | null; queries: Array | null } + | { type: "openPage"; url: string | null } + | { type: "findInPage"; url: string | null; pattern: string | null } + | { type: "other" }; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxReadiness.ts b/src/runtimes/openai/generated/v2/WindowsSandboxReadiness.ts new file mode 100644 index 0000000..41b1161 --- /dev/null +++ b/src/runtimes/openai/generated/v2/WindowsSandboxReadiness.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WindowsSandboxReadiness = "ready" | "notConfigured" | "updateRequired"; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxReadinessResponse.ts b/src/runtimes/openai/generated/v2/WindowsSandboxReadinessResponse.ts new file mode 100644 index 0000000..31e0d63 --- /dev/null +++ b/src/runtimes/openai/generated/v2/WindowsSandboxReadinessResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WindowsSandboxReadiness } from "./WindowsSandboxReadiness"; + +export type WindowsSandboxReadinessResponse = { status: WindowsSandboxReadiness }; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxSetupCompletedNotification.ts b/src/runtimes/openai/generated/v2/WindowsSandboxSetupCompletedNotification.ts new file mode 100644 index 0000000..374f381 --- /dev/null +++ b/src/runtimes/openai/generated/v2/WindowsSandboxSetupCompletedNotification.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; + +export type WindowsSandboxSetupCompletedNotification = { + mode: WindowsSandboxSetupMode; + success: boolean; + error: string | null; +}; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxSetupMode.ts b/src/runtimes/openai/generated/v2/WindowsSandboxSetupMode.ts new file mode 100644 index 0000000..a74bea4 --- /dev/null +++ b/src/runtimes/openai/generated/v2/WindowsSandboxSetupMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WindowsSandboxSetupMode = "elevated" | "unelevated"; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartParams.ts b/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartParams.ts new file mode 100644 index 0000000..3153a20 --- /dev/null +++ b/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartParams.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; + +export type WindowsSandboxSetupStartParams = { + mode: WindowsSandboxSetupMode; + cwd?: AbsolutePathBuf | null; +}; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartResponse.ts b/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartResponse.ts new file mode 100644 index 0000000..1aec14c --- /dev/null +++ b/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WindowsSandboxSetupStartResponse = { started: boolean }; diff --git a/src/runtimes/openai/generated/v2/WindowsWorldWritableWarningNotification.ts b/src/runtimes/openai/generated/v2/WindowsWorldWritableWarningNotification.ts new file mode 100644 index 0000000..b61824c --- /dev/null +++ b/src/runtimes/openai/generated/v2/WindowsWorldWritableWarningNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WindowsWorldWritableWarningNotification = { + samplePaths: Array; + extraCount: number; + failedScan: boolean; +}; diff --git a/src/runtimes/openai/generated/v2/WorkspaceMessage.ts b/src/runtimes/openai/generated/v2/WorkspaceMessage.ts new file mode 100644 index 0000000..f8d54d1 --- /dev/null +++ b/src/runtimes/openai/generated/v2/WorkspaceMessage.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WorkspaceMessageType } from "./WorkspaceMessageType"; + +export type WorkspaceMessage = { + messageId: string; + messageType: WorkspaceMessageType; + messageBody: string; + /** + * Unix timestamp (in seconds) when the message was created. + */ + createdAt: number | null; + /** + * Unix timestamp (in seconds) when the message was archived. + */ + archivedAt: number | null; +}; diff --git a/src/runtimes/openai/generated/v2/WorkspaceMessageType.ts b/src/runtimes/openai/generated/v2/WorkspaceMessageType.ts new file mode 100644 index 0000000..9d9438d --- /dev/null +++ b/src/runtimes/openai/generated/v2/WorkspaceMessageType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WorkspaceMessageType = "headline" | "announcement" | "unknown"; diff --git a/src/runtimes/openai/generated/v2/WriteStatus.ts b/src/runtimes/openai/generated/v2/WriteStatus.ts new file mode 100644 index 0000000..068eb3b --- /dev/null +++ b/src/runtimes/openai/generated/v2/WriteStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WriteStatus = "ok" | "okOverridden"; diff --git a/src/runtimes/openai/generated/v2/index.ts b/src/runtimes/openai/generated/v2/index.ts new file mode 100644 index 0000000..abe112f --- /dev/null +++ b/src/runtimes/openai/generated/v2/index.ts @@ -0,0 +1,714 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +export type { Account } from "./Account"; +export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification"; +export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification"; +export type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket"; +export type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary"; +export type { AccountUpdatedNotification } from "./AccountUpdatedNotification"; +export type { ActivePermissionProfile } from "./ActivePermissionProfile"; +export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType"; +export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus"; +export type { AdditionalContextEntry } from "./AdditionalContextEntry"; +export type { AdditionalContextKind } from "./AdditionalContextKind"; +export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; +export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; +export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; +export type { AgentMessageDelivery } from "./AgentMessageDelivery"; +export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification"; +export type { AllowDenyRequirement } from "./AllowDenyRequirement"; +export type { AnalyticsConfig } from "./AnalyticsConfig"; +export type { AppBranding } from "./AppBranding"; +export type { AppInfo } from "./AppInfo"; +export type { AppListUpdatedNotification } from "./AppListUpdatedNotification"; +export type { AppMetadata } from "./AppMetadata"; +export type { AppReview } from "./AppReview"; +export type { AppScreenshot } from "./AppScreenshot"; +export type { AppSummary } from "./AppSummary"; +export type { AppTemplateSummary } from "./AppTemplateSummary"; +export type { AppTemplateUnavailableReason } from "./AppTemplateUnavailableReason"; +export type { AppToolApproval } from "./AppToolApproval"; +export type { AppToolSummary } from "./AppToolSummary"; +export type { AppToolsConfig } from "./AppToolsConfig"; +export type { ApprovalsReviewer } from "./ApprovalsReviewer"; +export type { AppsConfig } from "./AppsConfig"; +export type { AppsDefaultConfig } from "./AppsDefaultConfig"; +export type { AppsInstalledParams } from "./AppsInstalledParams"; +export type { AppsInstalledResponse } from "./AppsInstalledResponse"; +export type { AppsListParams } from "./AppsListParams"; +export type { AppsListResponse } from "./AppsListResponse"; +export type { AppsReadParams } from "./AppsReadParams"; +export type { AppsReadResponse } from "./AppsReadResponse"; +export type { AskForApproval } from "./AskForApproval"; +export type { AttestationGenerateParams } from "./AttestationGenerateParams"; +export type { AttestationGenerateResponse } from "./AttestationGenerateResponse"; +export type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource"; +export type { AutoReviewRequirements } from "./AutoReviewRequirements"; +export type { AwsCredentialType } from "./AwsCredentialType"; +export type { BedrockAwsProfile } from "./BedrockAwsProfile"; +export type { BedrockDiscoverParams } from "./BedrockDiscoverParams"; +export type { BedrockDiscoverResponse } from "./BedrockDiscoverResponse"; +export type { BedrockEnvironmentCredential } from "./BedrockEnvironmentCredential"; +export type { BedrockSetupParams } from "./BedrockSetupParams"; +export type { BedrockSetupResponse } from "./BedrockSetupResponse"; +export type { BrowserUseAccessApprovalLifetime } from "./BrowserUseAccessApprovalLifetime"; +export type { BrowserUseConfig } from "./BrowserUseConfig"; +export type { BrowserUseOriginPolicy } from "./BrowserUseOriginPolicy"; +export type { BrowserUseOriginPolicyConfig } from "./BrowserUseOriginPolicyConfig"; +export type { BrowserUseRequirements } from "./BrowserUseRequirements"; +export type { ByteRange } from "./ByteRange"; +export type { CancelLoginAccountParams } from "./CancelLoginAccountParams"; +export type { CancelLoginAccountResponse } from "./CancelLoginAccountResponse"; +export type { CancelLoginAccountStatus } from "./CancelLoginAccountStatus"; +export type { CapabilityRootLocation } from "./CapabilityRootLocation"; +export type { ChatgptAuthTokensRefreshParams } from "./ChatgptAuthTokensRefreshParams"; +export type { ChatgptAuthTokensRefreshReason } from "./ChatgptAuthTokensRefreshReason"; +export type { ChatgptAuthTokensRefreshResponse } from "./ChatgptAuthTokensRefreshResponse"; +export type { CliAuthCredentialsStoreMode } from "./CliAuthCredentialsStoreMode"; +export type { CodexErrorInfo } from "./CodexErrorInfo"; +export type { CollabAgentState } from "./CollabAgentState"; +export type { CollabAgentStatus } from "./CollabAgentStatus"; +export type { CollabAgentTool } from "./CollabAgentTool"; +export type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; +export type { CollaborationModeListParams } from "./CollaborationModeListParams"; +export type { CollaborationModeListResponse } from "./CollaborationModeListResponse"; +export type { CollaborationModeMask } from "./CollaborationModeMask"; +export type { CommandAction } from "./CommandAction"; +export type { CommandExecOutputDeltaNotification } from "./CommandExecOutputDeltaNotification"; +export type { CommandExecOutputStream } from "./CommandExecOutputStream"; +export type { CommandExecParams } from "./CommandExecParams"; +export type { CommandExecResizeParams } from "./CommandExecResizeParams"; +export type { CommandExecResizeResponse } from "./CommandExecResizeResponse"; +export type { CommandExecResponse } from "./CommandExecResponse"; +export type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; +export type { CommandExecTerminateParams } from "./CommandExecTerminateParams"; +export type { CommandExecTerminateResponse } from "./CommandExecTerminateResponse"; +export type { CommandExecWriteParams } from "./CommandExecWriteParams"; +export type { CommandExecWriteResponse } from "./CommandExecWriteResponse"; +export type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; +export type { CommandExecutionApprovalKind } from "./CommandExecutionApprovalKind"; +export type { CommandExecutionOutputDeltaNotification } from "./CommandExecutionOutputDeltaNotification"; +export type { CommandExecutionRequestApprovalParams } from "./CommandExecutionRequestApprovalParams"; +export type { CommandExecutionRequestApprovalResponse } from "./CommandExecutionRequestApprovalResponse"; +export type { CommandExecutionSource } from "./CommandExecutionSource"; +export type { CommandExecutionStatus } from "./CommandExecutionStatus"; +export type { CommandMigration } from "./CommandMigration"; +export type { ComputerUseConfig } from "./ComputerUseConfig"; +export type { ComputerUseMacosConfig } from "./ComputerUseMacosConfig"; +export type { ComputerUseMacosRequirements } from "./ComputerUseMacosRequirements"; +export type { ComputerUseRequirements } from "./ComputerUseRequirements"; +export type { ComputerUseWindowsConfig } from "./ComputerUseWindowsConfig"; +export type { ComputerUseWindowsExeConfig } from "./ComputerUseWindowsExeConfig"; +export type { ComputerUseWindowsExeRequirement } from "./ComputerUseWindowsExeRequirement"; +export type { ComputerUseWindowsRequirements } from "./ComputerUseWindowsRequirements"; +export type { Config } from "./Config"; +export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams"; +export type { ConfigEdit } from "./ConfigEdit"; +export type { ConfigLayer } from "./ConfigLayer"; +export type { ConfigLayerMetadata } from "./ConfigLayerMetadata"; +export type { ConfigLayerSource } from "./ConfigLayerSource"; +export type { ConfigReadParams } from "./ConfigReadParams"; +export type { ConfigReadResponse } from "./ConfigReadResponse"; +export type { ConfigRequirements } from "./ConfigRequirements"; +export type { ConfigRequirementsReadResponse } from "./ConfigRequirementsReadResponse"; +export type { ConfigValueWriteParams } from "./ConfigValueWriteParams"; +export type { ConfigWarningNotification } from "./ConfigWarningNotification"; +export type { ConfigWriteResponse } from "./ConfigWriteResponse"; +export type { ConfiguredHookHandler } from "./ConfiguredHookHandler"; +export type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup"; +export type { ConnectorMetadata } from "./ConnectorMetadata"; +export type { ConsumeAccountRateLimitResetCreditOutcome } from "./ConsumeAccountRateLimitResetCreditOutcome"; +export type { ConsumeAccountRateLimitResetCreditParams } from "./ConsumeAccountRateLimitResetCreditParams"; +export type { ConsumeAccountRateLimitResetCreditResponse } from "./ConsumeAccountRateLimitResetCreditResponse"; +export type { ContextCompactedNotification } from "./ContextCompactedNotification"; +export type { CreditsSnapshot } from "./CreditsSnapshot"; +export type { CurrentTimeReadParams } from "./CurrentTimeReadParams"; +export type { CurrentTimeReadResponse } from "./CurrentTimeReadResponse"; +export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification"; +export type { DesktopOnboardingEntrypoint } from "./DesktopOnboardingEntrypoint"; +export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; +export type { DynamicToolCallParams } from "./DynamicToolCallParams"; +export type { DynamicToolCallResponse } from "./DynamicToolCallResponse"; +export type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; +export type { DynamicToolFunctionSpec } from "./DynamicToolFunctionSpec"; +export type { DynamicToolNamespaceSpec } from "./DynamicToolNamespaceSpec"; +export type { DynamicToolNamespaceTool } from "./DynamicToolNamespaceTool"; +export type { DynamicToolSpec } from "./DynamicToolSpec"; +export type { EnvironmentAddParams } from "./EnvironmentAddParams"; +export type { EnvironmentAddResponse } from "./EnvironmentAddResponse"; +export type { EnvironmentConnectionNotification } from "./EnvironmentConnectionNotification"; +export type { EnvironmentInfoParams } from "./EnvironmentInfoParams"; +export type { EnvironmentInfoResponse } from "./EnvironmentInfoResponse"; +export type { EnvironmentShellInfo } from "./EnvironmentShellInfo"; +export type { EnvironmentStatusKind } from "./EnvironmentStatusKind"; +export type { EnvironmentStatusParams } from "./EnvironmentStatusParams"; +export type { EnvironmentStatusResponse } from "./EnvironmentStatusResponse"; +export type { ErrorNotification } from "./ErrorNotification"; +export type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; +export type { ExperimentalFeature } from "./ExperimentalFeature"; +export type { ExperimentalFeatureEnablementSetParams } from "./ExperimentalFeatureEnablementSetParams"; +export type { ExperimentalFeatureEnablementSetResponse } from "./ExperimentalFeatureEnablementSetResponse"; +export type { ExperimentalFeatureListParams } from "./ExperimentalFeatureListParams"; +export type { ExperimentalFeatureListResponse } from "./ExperimentalFeatureListResponse"; +export type { ExperimentalFeatureStage } from "./ExperimentalFeatureStage"; +export type { ExternalAgentConfigDetectParams } from "./ExternalAgentConfigDetectParams"; +export type { ExternalAgentConfigDetectResponse } from "./ExternalAgentConfigDetectResponse"; +export type { ExternalAgentConfigImportCompletedNotification } from "./ExternalAgentConfigImportCompletedNotification"; +export type { ExternalAgentConfigImportHistoriesReadResponse } from "./ExternalAgentConfigImportHistoriesReadResponse"; +export type { ExternalAgentConfigImportHistory } from "./ExternalAgentConfigImportHistory"; +export type { ExternalAgentConfigImportHistoryRecordParams } from "./ExternalAgentConfigImportHistoryRecordParams"; +export type { ExternalAgentConfigImportHistoryRecordResponse } from "./ExternalAgentConfigImportHistoryRecordResponse"; +export type { ExternalAgentConfigImportHistoryRecordSuccessParams } from "./ExternalAgentConfigImportHistoryRecordSuccessParams"; +export type { ExternalAgentConfigImportHistoryRecordTypeResultParams } from "./ExternalAgentConfigImportHistoryRecordTypeResultParams"; +export type { ExternalAgentConfigImportItemTypeFailure } from "./ExternalAgentConfigImportItemTypeFailure"; +export type { ExternalAgentConfigImportItemTypeSuccess } from "./ExternalAgentConfigImportItemTypeSuccess"; +export type { ExternalAgentConfigImportParams } from "./ExternalAgentConfigImportParams"; +export type { ExternalAgentConfigImportProgressNotification } from "./ExternalAgentConfigImportProgressNotification"; +export type { ExternalAgentConfigImportResponse } from "./ExternalAgentConfigImportResponse"; +export type { ExternalAgentConfigImportTypeResult } from "./ExternalAgentConfigImportTypeResult"; +export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; +export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; +export type { ExternalAgentDetectedConnectorCandidate } from "./ExternalAgentDetectedConnectorCandidate"; +export type { ExternalAgentDetectedConnectorSource } from "./ExternalAgentDetectedConnectorSource"; +export type { ExternalAgentImportedConnectorCandidate } from "./ExternalAgentImportedConnectorCandidate"; +export type { ExternalAgentImportedConnectorSource } from "./ExternalAgentImportedConnectorSource"; +export type { FeedbackRequirements } from "./FeedbackRequirements"; +export type { FeedbackUploadParams } from "./FeedbackUploadParams"; +export type { FeedbackUploadResponse } from "./FeedbackUploadResponse"; +export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision"; +export type { FileChangeOutputDeltaNotification } from "./FileChangeOutputDeltaNotification"; +export type { FileChangePatchUpdatedNotification } from "./FileChangePatchUpdatedNotification"; +export type { FileChangeRequestApprovalParams } from "./FileChangeRequestApprovalParams"; +export type { FileChangeRequestApprovalResponse } from "./FileChangeRequestApprovalResponse"; +export type { FileSystemAccessMode } from "./FileSystemAccessMode"; +export type { FileSystemPath } from "./FileSystemPath"; +export type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry"; +export type { FileSystemSpecialPath } from "./FileSystemSpecialPath"; +export type { FileUpdateChange } from "./FileUpdateChange"; +export type { ForcedChatgptWorkspaceIds } from "./ForcedChatgptWorkspaceIds"; +export type { FsChangedNotification } from "./FsChangedNotification"; +export type { FsCopyParams } from "./FsCopyParams"; +export type { FsCopyResponse } from "./FsCopyResponse"; +export type { FsCreateDirectoryParams } from "./FsCreateDirectoryParams"; +export type { FsCreateDirectoryResponse } from "./FsCreateDirectoryResponse"; +export type { FsGetMetadataParams } from "./FsGetMetadataParams"; +export type { FsGetMetadataResponse } from "./FsGetMetadataResponse"; +export type { FsReadDirectoryEntry } from "./FsReadDirectoryEntry"; +export type { FsReadDirectoryParams } from "./FsReadDirectoryParams"; +export type { FsReadDirectoryResponse } from "./FsReadDirectoryResponse"; +export type { FsReadFileParams } from "./FsReadFileParams"; +export type { FsReadFileResponse } from "./FsReadFileResponse"; +export type { FsRemoveParams } from "./FsRemoveParams"; +export type { FsRemoveResponse } from "./FsRemoveResponse"; +export type { FsUnwatchParams } from "./FsUnwatchParams"; +export type { FsUnwatchResponse } from "./FsUnwatchResponse"; +export type { FsWatchParams } from "./FsWatchParams"; +export type { FsWatchResponse } from "./FsWatchResponse"; +export type { FsWriteFileParams } from "./FsWriteFileParams"; +export type { FsWriteFileResponse } from "./FsWriteFileResponse"; +export type { GetAccountParams } from "./GetAccountParams"; +export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse"; +export type { GetAccountResponse } from "./GetAccountResponse"; +export type { GetAccountTokenUsageParams } from "./GetAccountTokenUsageParams"; +export type { GetAccountTokenUsageResponse } from "./GetAccountTokenUsageResponse"; +export type { GetWorkspaceMessagesResponse } from "./GetWorkspaceMessagesResponse"; +export type { GitInfo } from "./GitInfo"; +export type { GrantedPermissionProfile } from "./GrantedPermissionProfile"; +export type { GuardianApprovalReview } from "./GuardianApprovalReview"; +export type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction"; +export type { GuardianApprovalReviewStatus } from "./GuardianApprovalReviewStatus"; +export type { GuardianCommandSource } from "./GuardianCommandSource"; +export type { GuardianRiskLevel } from "./GuardianRiskLevel"; +export type { GuardianUserAuthorization } from "./GuardianUserAuthorization"; +export type { GuardianWarningNotification } from "./GuardianWarningNotification"; +export type { HookCompletedNotification } from "./HookCompletedNotification"; +export type { HookErrorInfo } from "./HookErrorInfo"; +export type { HookEventName } from "./HookEventName"; +export type { HookExecutionMode } from "./HookExecutionMode"; +export type { HookHandlerType } from "./HookHandlerType"; +export type { HookMetadata } from "./HookMetadata"; +export type { HookMigration } from "./HookMigration"; +export type { HookOutputEntry } from "./HookOutputEntry"; +export type { HookOutputEntryKind } from "./HookOutputEntryKind"; +export type { HookPromptFragment } from "./HookPromptFragment"; +export type { HookRunStatus } from "./HookRunStatus"; +export type { HookRunSummary } from "./HookRunSummary"; +export type { HookScope } from "./HookScope"; +export type { HookSource } from "./HookSource"; +export type { HookStartedNotification } from "./HookStartedNotification"; +export type { HookTrustStatus } from "./HookTrustStatus"; +export type { HooksListEntry } from "./HooksListEntry"; +export type { HooksListParams } from "./HooksListParams"; +export type { HooksListResponse } from "./HooksListResponse"; +export type { InAppBrowserRequirements } from "./InAppBrowserRequirements"; +export type { InstalledApp } from "./InstalledApp"; +export type { ItemCompletedNotification } from "./ItemCompletedNotification"; +export type { ItemGuardianApprovalReviewCompletedNotification } from "./ItemGuardianApprovalReviewCompletedNotification"; +export type { ItemGuardianApprovalReviewStartedNotification } from "./ItemGuardianApprovalReviewStartedNotification"; +export type { ItemStartedNotification } from "./ItemStartedNotification"; +export type { ListMcpServerStatusParams } from "./ListMcpServerStatusParams"; +export type { ListMcpServerStatusResponse } from "./ListMcpServerStatusResponse"; +export type { LoginAccountParams } from "./LoginAccountParams"; +export type { LoginAccountResponse } from "./LoginAccountResponse"; +export type { LoginAppBrand } from "./LoginAppBrand"; +export type { LogoutAccountResponse } from "./LogoutAccountResponse"; +export type { ManagedHooksRequirements } from "./ManagedHooksRequirements"; +export type { MarketplaceAddParams } from "./MarketplaceAddParams"; +export type { MarketplaceAddResponse } from "./MarketplaceAddResponse"; +export type { MarketplaceInterface } from "./MarketplaceInterface"; +export type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo"; +export type { MarketplaceRemoveParams } from "./MarketplaceRemoveParams"; +export type { MarketplaceRemoveResponse } from "./MarketplaceRemoveResponse"; +export type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo"; +export type { MarketplaceUpgradeParams } from "./MarketplaceUpgradeParams"; +export type { MarketplaceUpgradeResponse } from "./MarketplaceUpgradeResponse"; +export type { McpAuthStatus } from "./McpAuthStatus"; +export type { McpElicitationArrayType } from "./McpElicitationArrayType"; +export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema"; +export type { McpElicitationBooleanType } from "./McpElicitationBooleanType"; +export type { McpElicitationConstOption } from "./McpElicitationConstOption"; +export type { McpElicitationEnumSchema } from "./McpElicitationEnumSchema"; +export type { McpElicitationLegacyTitledEnumSchema } from "./McpElicitationLegacyTitledEnumSchema"; +export type { McpElicitationMultiSelectEnumSchema } from "./McpElicitationMultiSelectEnumSchema"; +export type { McpElicitationNumberSchema } from "./McpElicitationNumberSchema"; +export type { McpElicitationNumberType } from "./McpElicitationNumberType"; +export type { McpElicitationObjectType } from "./McpElicitationObjectType"; +export type { McpElicitationPrimitiveSchema } from "./McpElicitationPrimitiveSchema"; +export type { McpElicitationSchema } from "./McpElicitationSchema"; +export type { McpElicitationSingleSelectEnumSchema } from "./McpElicitationSingleSelectEnumSchema"; +export type { McpElicitationStringFormat } from "./McpElicitationStringFormat"; +export type { McpElicitationStringSchema } from "./McpElicitationStringSchema"; +export type { McpElicitationStringType } from "./McpElicitationStringType"; +export type { McpElicitationTitledEnumItems } from "./McpElicitationTitledEnumItems"; +export type { McpElicitationTitledMultiSelectEnumSchema } from "./McpElicitationTitledMultiSelectEnumSchema"; +export type { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitationTitledSingleSelectEnumSchema"; +export type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems"; +export type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema"; +export type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema"; +export type { McpResourceReadParams } from "./McpResourceReadParams"; +export type { McpResourceReadResponse } from "./McpResourceReadResponse"; +export type { McpServerConnectionStatus } from "./McpServerConnectionStatus"; +export type { McpServerElicitationAction } from "./McpServerElicitationAction"; +export type { McpServerElicitationRequestParams } from "./McpServerElicitationRequestParams"; +export type { McpServerElicitationRequestResponse } from "./McpServerElicitationRequestResponse"; +export type { McpServerEventNotification } from "./McpServerEventNotification"; +export type { McpServerEventStreamNotification } from "./McpServerEventStreamNotification"; +export type { McpServerEventStreamStartParams } from "./McpServerEventStreamStartParams"; +export type { McpServerEventStreamStartResponse } from "./McpServerEventStreamStartResponse"; +export type { McpServerEventStreamStopParams } from "./McpServerEventStreamStopParams"; +export type { McpServerEventStreamStopResponse } from "./McpServerEventStreamStopResponse"; +export type { McpServerMigration } from "./McpServerMigration"; +export type { McpServerOauthClientRegistration } from "./McpServerOauthClientRegistration"; +export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthLoginCompletedNotification"; +export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams"; +export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse"; +export type { McpServerRefreshResponse } from "./McpServerRefreshResponse"; +export type { McpServerStartupFailureReason } from "./McpServerStartupFailureReason"; +export type { McpServerStartupState } from "./McpServerStartupState"; +export type { McpServerStatus } from "./McpServerStatus"; +export type { McpServerStatusDetail } from "./McpServerStatusDetail"; +export type { McpServerStatusUpdatedNotification } from "./McpServerStatusUpdatedNotification"; +export type { McpServerToolCallParams } from "./McpServerToolCallParams"; +export type { McpServerToolCallResponse } from "./McpServerToolCallResponse"; +export type { McpToolCallAppContext } from "./McpToolCallAppContext"; +export type { McpToolCallError } from "./McpToolCallError"; +export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification"; +export type { McpToolCallResult } from "./McpToolCallResult"; +export type { McpToolCallStatus } from "./McpToolCallStatus"; +export type { MemoryCitation } from "./MemoryCitation"; +export type { MemoryCitationEntry } from "./MemoryCitationEntry"; +export type { MemoryResetResponse } from "./MemoryResetResponse"; +export type { MergeStrategy } from "./MergeStrategy"; +export type { MigrationDetails } from "./MigrationDetails"; +export type { MockExperimentalMethodParams } from "./MockExperimentalMethodParams"; +export type { MockExperimentalMethodResponse } from "./MockExperimentalMethodResponse"; +export type { Model } from "./Model"; +export type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; +export type { ModelListParams } from "./ModelListParams"; +export type { ModelListResponse } from "./ModelListResponse"; +export type { ModelProviderCapabilitiesReadParams } from "./ModelProviderCapabilitiesReadParams"; +export type { ModelProviderCapabilitiesReadResponse } from "./ModelProviderCapabilitiesReadResponse"; +export type { ModelRerouteReason } from "./ModelRerouteReason"; +export type { ModelReroutedNotification } from "./ModelReroutedNotification"; +export type { ModelSafetyBufferingUpdatedNotification } from "./ModelSafetyBufferingUpdatedNotification"; +export type { ModelServiceTier } from "./ModelServiceTier"; +export type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; +export type { ModelVerification } from "./ModelVerification"; +export type { ModelVerificationNotification } from "./ModelVerificationNotification"; +export type { ModelsRequirements } from "./ModelsRequirements"; +export type { MultiAgentVersion } from "./MultiAgentVersion"; +export type { NetworkAccess } from "./NetworkAccess"; +export type { NetworkApprovalContext } from "./NetworkApprovalContext"; +export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; +export type { NetworkDomainPermission } from "./NetworkDomainPermission"; +export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; +export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; +export type { NetworkRequirements } from "./NetworkRequirements"; +export type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission"; +export type { NewThreadModelDefaults } from "./NewThreadModelDefaults"; +export type { NonSteerableTurnKind } from "./NonSteerableTurnKind"; +export type { OverriddenMetadata } from "./OverriddenMetadata"; +export type { PatchApplyStatus } from "./PatchApplyStatus"; +export type { PatchChangeKind } from "./PatchChangeKind"; +export type { PermissionGrantScope } from "./PermissionGrantScope"; +export type { PermissionProfileListParams } from "./PermissionProfileListParams"; +export type { PermissionProfileListResponse } from "./PermissionProfileListResponse"; +export type { PermissionProfileSummary } from "./PermissionProfileSummary"; +export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams"; +export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse"; +export type { PlanDeltaNotification } from "./PlanDeltaNotification"; +export type { PluginAuthPolicy } from "./PluginAuthPolicy"; +export type { PluginAvailability } from "./PluginAvailability"; +export type { PluginDetail } from "./PluginDetail"; +export type { PluginDisabledReason } from "./PluginDisabledReason"; +export type { PluginHookSummary } from "./PluginHookSummary"; +export type { PluginInstallParams } from "./PluginInstallParams"; +export type { PluginInstallPolicy } from "./PluginInstallPolicy"; +export type { PluginInstallPolicySource } from "./PluginInstallPolicySource"; +export type { PluginInstallResponse } from "./PluginInstallResponse"; +export type { PluginInstalledParams } from "./PluginInstalledParams"; +export type { PluginInstalledResponse } from "./PluginInstalledResponse"; +export type { PluginInterface } from "./PluginInterface"; +export type { PluginListMarketplaceKind } from "./PluginListMarketplaceKind"; +export type { PluginListParams } from "./PluginListParams"; +export type { PluginListResponse } from "./PluginListResponse"; +export type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry"; +export type { PluginReadParams } from "./PluginReadParams"; +export type { PluginReadResponse } from "./PluginReadResponse"; +export type { PluginSearchParams } from "./PluginSearchParams"; +export type { PluginSearchResponse } from "./PluginSearchResponse"; +export type { PluginSearchResult } from "./PluginSearchResult"; +export type { PluginSearchScope } from "./PluginSearchScope"; +export type { PluginShareCheckoutParams } from "./PluginShareCheckoutParams"; +export type { PluginShareCheckoutResponse } from "./PluginShareCheckoutResponse"; +export type { PluginShareContext } from "./PluginShareContext"; +export type { PluginShareDeleteParams } from "./PluginShareDeleteParams"; +export type { PluginShareDeleteResponse } from "./PluginShareDeleteResponse"; +export type { PluginShareDiscoverability } from "./PluginShareDiscoverability"; +export type { PluginShareListItem } from "./PluginShareListItem"; +export type { PluginShareListParams } from "./PluginShareListParams"; +export type { PluginShareListResponse } from "./PluginShareListResponse"; +export type { PluginSharePrincipal } from "./PluginSharePrincipal"; +export type { PluginSharePrincipalRole } from "./PluginSharePrincipalRole"; +export type { PluginSharePrincipalType } from "./PluginSharePrincipalType"; +export type { PluginShareSaveParams } from "./PluginShareSaveParams"; +export type { PluginShareSaveResponse } from "./PluginShareSaveResponse"; +export type { PluginShareTarget } from "./PluginShareTarget"; +export type { PluginShareTargetRole } from "./PluginShareTargetRole"; +export type { PluginShareUpdateDiscoverability } from "./PluginShareUpdateDiscoverability"; +export type { PluginShareUpdateTargetsParams } from "./PluginShareUpdateTargetsParams"; +export type { PluginShareUpdateTargetsResponse } from "./PluginShareUpdateTargetsResponse"; +export type { PluginSkillReadParams } from "./PluginSkillReadParams"; +export type { PluginSkillReadResponse } from "./PluginSkillReadResponse"; +export type { PluginSource } from "./PluginSource"; +export type { PluginSummary } from "./PluginSummary"; +export type { PluginUninstallParams } from "./PluginUninstallParams"; +export type { PluginUninstallResponse } from "./PluginUninstallResponse"; +export type { PluginsMigration } from "./PluginsMigration"; +export type { ProcessExitedNotification } from "./ProcessExitedNotification"; +export type { ProcessKillParams } from "./ProcessKillParams"; +export type { ProcessKillResponse } from "./ProcessKillResponse"; +export type { ProcessOutputDeltaNotification } from "./ProcessOutputDeltaNotification"; +export type { ProcessOutputStream } from "./ProcessOutputStream"; +export type { ProcessResizePtyParams } from "./ProcessResizePtyParams"; +export type { ProcessResizePtyResponse } from "./ProcessResizePtyResponse"; +export type { ProcessSpawnParams } from "./ProcessSpawnParams"; +export type { ProcessSpawnResponse } from "./ProcessSpawnResponse"; +export type { ProcessTerminalSize } from "./ProcessTerminalSize"; +export type { ProcessWriteStdinParams } from "./ProcessWriteStdinParams"; +export type { ProcessWriteStdinResponse } from "./ProcessWriteStdinResponse"; +export type { Project } from "./Project"; +export type { ProjectChangeType } from "./ProjectChangeType"; +export type { ProjectChangedNotification } from "./ProjectChangedNotification"; +export type { ProjectCreateParams } from "./ProjectCreateParams"; +export type { ProjectCreateResponse } from "./ProjectCreateResponse"; +export type { ProjectDeleteParams } from "./ProjectDeleteParams"; +export type { ProjectDeleteResponse } from "./ProjectDeleteResponse"; +export type { ProjectImportParams } from "./ProjectImportParams"; +export type { ProjectImportResponse } from "./ProjectImportResponse"; +export type { ProjectListParams } from "./ProjectListParams"; +export type { ProjectListResponse } from "./ProjectListResponse"; +export type { ProjectMoveParams } from "./ProjectMoveParams"; +export type { ProjectMoveResponse } from "./ProjectMoveResponse"; +export type { ProjectReadParams } from "./ProjectReadParams"; +export type { ProjectReadResponse } from "./ProjectReadResponse"; +export type { ProjectRoot } from "./ProjectRoot"; +export type { ProjectUpdateParams } from "./ProjectUpdateParams"; +export type { ProjectUpdateResponse } from "./ProjectUpdateResponse"; +export type { QueuedSubmission } from "./QueuedSubmission"; +export type { RateLimitReachedType } from "./RateLimitReachedType"; +export type { RateLimitResetCredit } from "./RateLimitResetCredit"; +export type { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus"; +export type { RateLimitResetCreditsSummary } from "./RateLimitResetCreditsSummary"; +export type { RateLimitResetType } from "./RateLimitResetType"; +export type { RateLimitSnapshot } from "./RateLimitSnapshot"; +export type { RateLimitWindow } from "./RateLimitWindow"; +export type { RawResponseCompletedNotification } from "./RawResponseCompletedNotification"; +export type { RawResponseItemCompletedNotification } from "./RawResponseItemCompletedNotification"; +export type { ReasoningEffortOption } from "./ReasoningEffortOption"; +export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification"; +export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification"; +export type { ReasoningTextDeltaNotification } from "./ReasoningTextDeltaNotification"; +export type { RemoteControlClient } from "./RemoteControlClient"; +export type { RemoteControlClientsListOrder } from "./RemoteControlClientsListOrder"; +export type { RemoteControlClientsListParams } from "./RemoteControlClientsListParams"; +export type { RemoteControlClientsListResponse } from "./RemoteControlClientsListResponse"; +export type { RemoteControlClientsRevokeParams } from "./RemoteControlClientsRevokeParams"; +export type { RemoteControlClientsRevokeResponse } from "./RemoteControlClientsRevokeResponse"; +export type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; +export type { RemoteControlDisableParams } from "./RemoteControlDisableParams"; +export type { RemoteControlDisableResponse } from "./RemoteControlDisableResponse"; +export type { RemoteControlEnableParams } from "./RemoteControlEnableParams"; +export type { RemoteControlEnableResponse } from "./RemoteControlEnableResponse"; +export type { RemoteControlPairingStartParams } from "./RemoteControlPairingStartParams"; +export type { RemoteControlPairingStartResponse } from "./RemoteControlPairingStartResponse"; +export type { RemoteControlPairingStatusParams } from "./RemoteControlPairingStatusParams"; +export type { RemoteControlPairingStatusResponse } from "./RemoteControlPairingStatusResponse"; +export type { RemoteControlStatusChangedNotification } from "./RemoteControlStatusChangedNotification"; +export type { RemoteControlStatusReadResponse } from "./RemoteControlStatusReadResponse"; +export type { RequestPermissionProfile } from "./RequestPermissionProfile"; +export type { ResidencyRequirement } from "./ResidencyRequirement"; +export type { ReviewDelivery } from "./ReviewDelivery"; +export type { ReviewStartParams } from "./ReviewStartParams"; +export type { ReviewStartResponse } from "./ReviewStartResponse"; +export type { ReviewTarget } from "./ReviewTarget"; +export type { SandboxMode } from "./SandboxMode"; +export type { SandboxPolicy } from "./SandboxPolicy"; +export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; +export type { ScheduledTaskSchedule } from "./ScheduledTaskSchedule"; +export type { ScheduledTaskSummary } from "./ScheduledTaskSummary"; +export type { ScheduledTaskWeekday } from "./ScheduledTaskWeekday"; +export type { SelectedCapabilityRoot } from "./SelectedCapabilityRoot"; +export type { SendAddCreditsNudgeEmailParams } from "./SendAddCreditsNudgeEmailParams"; +export type { SendAddCreditsNudgeEmailResponse } from "./SendAddCreditsNudgeEmailResponse"; +export type { ServerDiagnosticsGauge } from "./ServerDiagnosticsGauge"; +export type { ServerDiagnosticsParams } from "./ServerDiagnosticsParams"; +export type { ServerDiagnosticsProcess } from "./ServerDiagnosticsProcess"; +export type { ServerDiagnosticsResponse } from "./ServerDiagnosticsResponse"; +export type { ServerRequestResolvedNotification } from "./ServerRequestResolvedNotification"; +export type { SessionMigration } from "./SessionMigration"; +export type { SessionSource } from "./SessionSource"; +export type { SkillDependencies } from "./SkillDependencies"; +export type { SkillErrorInfo } from "./SkillErrorInfo"; +export type { SkillInterface } from "./SkillInterface"; +export type { SkillMetadata } from "./SkillMetadata"; +export type { SkillMigration } from "./SkillMigration"; +export type { SkillScope } from "./SkillScope"; +export type { SkillSummary } from "./SkillSummary"; +export type { SkillToolDependency } from "./SkillToolDependency"; +export type { SkillsChangedNotification } from "./SkillsChangedNotification"; +export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams"; +export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse"; +export type { SkillsExtraRootsSetParams } from "./SkillsExtraRootsSetParams"; +export type { SkillsExtraRootsSetResponse } from "./SkillsExtraRootsSetResponse"; +export type { SkillsListEntry } from "./SkillsListEntry"; +export type { SkillsListParams } from "./SkillsListParams"; +export type { SkillsListResponse } from "./SkillsListResponse"; +export type { SortDirection } from "./SortDirection"; +export type { SpendControlLimitSnapshot } from "./SpendControlLimitSnapshot"; +export type { StrictReviewRequiredNotification } from "./StrictReviewRequiredNotification"; +export type { SubAgentActivityKind } from "./SubAgentActivityKind"; +export type { SubagentMigration } from "./SubagentMigration"; +export type { TerminalInteractionNotification } from "./TerminalInteractionNotification"; +export type { TextElement } from "./TextElement"; +export type { TextPosition } from "./TextPosition"; +export type { TextRange } from "./TextRange"; +export type { Thread } from "./Thread"; +export type { ThreadActiveFlag } from "./ThreadActiveFlag"; +export type { ThreadApproveGuardianDeniedActionParams } from "./ThreadApproveGuardianDeniedActionParams"; +export type { ThreadApproveGuardianDeniedActionResponse } from "./ThreadApproveGuardianDeniedActionResponse"; +export type { ThreadArchiveParams } from "./ThreadArchiveParams"; +export type { ThreadArchiveResponse } from "./ThreadArchiveResponse"; +export type { ThreadArchivedNotification } from "./ThreadArchivedNotification"; +export type { ThreadBackgroundTerminal } from "./ThreadBackgroundTerminal"; +export type { ThreadBackgroundTerminalsCleanParams } from "./ThreadBackgroundTerminalsCleanParams"; +export type { ThreadBackgroundTerminalsCleanResponse } from "./ThreadBackgroundTerminalsCleanResponse"; +export type { ThreadBackgroundTerminalsListParams } from "./ThreadBackgroundTerminalsListParams"; +export type { ThreadBackgroundTerminalsListResponse } from "./ThreadBackgroundTerminalsListResponse"; +export type { ThreadBackgroundTerminalsTerminateParams } from "./ThreadBackgroundTerminalsTerminateParams"; +export type { ThreadBackgroundTerminalsTerminateResponse } from "./ThreadBackgroundTerminalsTerminateResponse"; +export type { ThreadClosedNotification } from "./ThreadClosedNotification"; +export type { ThreadCompactStartParams } from "./ThreadCompactStartParams"; +export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse"; +export type { ThreadDecrementElicitationParams } from "./ThreadDecrementElicitationParams"; +export type { ThreadDecrementElicitationResponse } from "./ThreadDecrementElicitationResponse"; +export type { ThreadDeleteParams } from "./ThreadDeleteParams"; +export type { ThreadDeleteResponse } from "./ThreadDeleteResponse"; +export type { ThreadDeletedNotification } from "./ThreadDeletedNotification"; +export type { ThreadExtra } from "./ThreadExtra"; +export type { ThreadForkParams } from "./ThreadForkParams"; +export type { ThreadForkResponse } from "./ThreadForkResponse"; +export type { ThreadGoal } from "./ThreadGoal"; +export type { ThreadGoalClearParams } from "./ThreadGoalClearParams"; +export type { ThreadGoalClearResponse } from "./ThreadGoalClearResponse"; +export type { ThreadGoalClearedNotification } from "./ThreadGoalClearedNotification"; +export type { ThreadGoalGetParams } from "./ThreadGoalGetParams"; +export type { ThreadGoalGetResponse } from "./ThreadGoalGetResponse"; +export type { ThreadGoalSetParams } from "./ThreadGoalSetParams"; +export type { ThreadGoalSetResponse } from "./ThreadGoalSetResponse"; +export type { ThreadGoalStatus } from "./ThreadGoalStatus"; +export type { ThreadGoalUpdatedNotification } from "./ThreadGoalUpdatedNotification"; +export type { ThreadHistoryMode } from "./ThreadHistoryMode"; +export type { ThreadIncrementElicitationParams } from "./ThreadIncrementElicitationParams"; +export type { ThreadIncrementElicitationResponse } from "./ThreadIncrementElicitationResponse"; +export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams"; +export type { ThreadInjectItemsResponse } from "./ThreadInjectItemsResponse"; +export type { ThreadItem } from "./ThreadItem"; +export type { ThreadItemEntry } from "./ThreadItemEntry"; +export type { ThreadItemsListParams } from "./ThreadItemsListParams"; +export type { ThreadItemsListResponse } from "./ThreadItemsListResponse"; +export type { ThreadListParams } from "./ThreadListParams"; +export type { ThreadListResponse } from "./ThreadListResponse"; +export type { ThreadLoadedListParams } from "./ThreadLoadedListParams"; +export type { ThreadLoadedListResponse } from "./ThreadLoadedListResponse"; +export type { ThreadMemoryModeSetParams } from "./ThreadMemoryModeSetParams"; +export type { ThreadMemoryModeSetResponse } from "./ThreadMemoryModeSetResponse"; +export type { ThreadMetadataGitInfoUpdateParams } from "./ThreadMetadataGitInfoUpdateParams"; +export type { ThreadMetadataUpdateParams } from "./ThreadMetadataUpdateParams"; +export type { ThreadMetadataUpdateResponse } from "./ThreadMetadataUpdateResponse"; +export type { ThreadNameUpdatedNotification } from "./ThreadNameUpdatedNotification"; +export type { ThreadProjectUpdatedNotification } from "./ThreadProjectUpdatedNotification"; +export type { ThreadQueueAddParams } from "./ThreadQueueAddParams"; +export type { ThreadQueueAddResponse } from "./ThreadQueueAddResponse"; +export type { ThreadQueueChangedNotification } from "./ThreadQueueChangedNotification"; +export type { ThreadQueueDeleteParams } from "./ThreadQueueDeleteParams"; +export type { ThreadQueueDeleteResponse } from "./ThreadQueueDeleteResponse"; +export type { ThreadQueueListParams } from "./ThreadQueueListParams"; +export type { ThreadQueueListResponse } from "./ThreadQueueListResponse"; +export type { ThreadQueueReorderParams } from "./ThreadQueueReorderParams"; +export type { ThreadQueueReorderResponse } from "./ThreadQueueReorderResponse"; +export type { ThreadQueueStartParams } from "./ThreadQueueStartParams"; +export type { ThreadQueueStartResponse } from "./ThreadQueueStartResponse"; +export type { ThreadQueueUpdateParams } from "./ThreadQueueUpdateParams"; +export type { ThreadQueueUpdateResponse } from "./ThreadQueueUpdateResponse"; +export type { ThreadReadParams } from "./ThreadReadParams"; +export type { ThreadReadResponse } from "./ThreadReadResponse"; +export type { ThreadRealtimeAppendAudioParams } from "./ThreadRealtimeAppendAudioParams"; +export type { ThreadRealtimeAppendAudioResponse } from "./ThreadRealtimeAppendAudioResponse"; +export type { ThreadRealtimeAppendSpeechParams } from "./ThreadRealtimeAppendSpeechParams"; +export type { ThreadRealtimeAppendSpeechResponse } from "./ThreadRealtimeAppendSpeechResponse"; +export type { ThreadRealtimeAppendTextParams } from "./ThreadRealtimeAppendTextParams"; +export type { ThreadRealtimeAppendTextResponse } from "./ThreadRealtimeAppendTextResponse"; +export type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk"; +export type { ThreadRealtimeBemItemPresentation } from "./ThreadRealtimeBemItemPresentation"; +export type { ThreadRealtimeClosedNotification } from "./ThreadRealtimeClosedNotification"; +export type { ThreadRealtimeErrorNotification } from "./ThreadRealtimeErrorNotification"; +export type { ThreadRealtimeInitialItem } from "./ThreadRealtimeInitialItem"; +export type { ThreadRealtimeItem } from "./ThreadRealtimeItem"; +export type { ThreadRealtimeItemAddedNotification } from "./ThreadRealtimeItemAddedNotification"; +export type { ThreadRealtimeItemCompletedNotification } from "./ThreadRealtimeItemCompletedNotification"; +export type { ThreadRealtimeItemStartedNotification } from "./ThreadRealtimeItemStartedNotification"; +export type { ThreadRealtimeItemTranscriptDeltaNotification } from "./ThreadRealtimeItemTranscriptDeltaNotification"; +export type { ThreadRealtimeListVoicesParams } from "./ThreadRealtimeListVoicesParams"; +export type { ThreadRealtimeListVoicesResponse } from "./ThreadRealtimeListVoicesResponse"; +export type { ThreadRealtimeOutputAudioDeltaNotification } from "./ThreadRealtimeOutputAudioDeltaNotification"; +export type { ThreadRealtimeSdpNotification } from "./ThreadRealtimeSdpNotification"; +export type { ThreadRealtimeSessionOutcome } from "./ThreadRealtimeSessionOutcome"; +export type { ThreadRealtimeStartParams } from "./ThreadRealtimeStartParams"; +export type { ThreadRealtimeStartResponse } from "./ThreadRealtimeStartResponse"; +export type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTransport"; +export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification"; +export type { ThreadRealtimeStopParams } from "./ThreadRealtimeStopParams"; +export type { ThreadRealtimeStopResponse } from "./ThreadRealtimeStopResponse"; +export type { ThreadRealtimeTranscriptDeltaNotification } from "./ThreadRealtimeTranscriptDeltaNotification"; +export type { ThreadRealtimeTranscriptDoneNotification } from "./ThreadRealtimeTranscriptDoneNotification"; +export type { ThreadRealtimeTranscriptRole } from "./ThreadRealtimeTranscriptRole"; +export type { ThreadResumeInitialTurnsPageParams } from "./ThreadResumeInitialTurnsPageParams"; +export type { ThreadResumeParams } from "./ThreadResumeParams"; +export type { ThreadResumeResponse } from "./ThreadResumeResponse"; +export type { ThreadRevertParams } from "./ThreadRevertParams"; +export type { ThreadRevertResponse } from "./ThreadRevertResponse"; +export type { ThreadRevertedNotification } from "./ThreadRevertedNotification"; +export type { ThreadRollbackParams } from "./ThreadRollbackParams"; +export type { ThreadRollbackResponse } from "./ThreadRollbackResponse"; +export type { ThreadSearchOccurrence } from "./ThreadSearchOccurrence"; +export type { ThreadSearchOccurrencesParams } from "./ThreadSearchOccurrencesParams"; +export type { ThreadSearchOccurrencesResponse } from "./ThreadSearchOccurrencesResponse"; +export type { ThreadSearchParams } from "./ThreadSearchParams"; +export type { ThreadSearchResponse } from "./ThreadSearchResponse"; +export type { ThreadSearchResult } from "./ThreadSearchResult"; +export type { ThreadSearchSortKey } from "./ThreadSearchSortKey"; +export type { ThreadSearchTextRange } from "./ThreadSearchTextRange"; +export type { ThreadSection } from "./ThreadSection"; +export type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; +export type { ThreadSectionCreateParams } from "./ThreadSectionCreateParams"; +export type { ThreadSectionCreateResponse } from "./ThreadSectionCreateResponse"; +export type { ThreadSectionDeleteParams } from "./ThreadSectionDeleteParams"; +export type { ThreadSectionDeleteResponse } from "./ThreadSectionDeleteResponse"; +export type { ThreadSectionListParams } from "./ThreadSectionListParams"; +export type { ThreadSectionListResponse } from "./ThreadSectionListResponse"; +export type { ThreadSectionMoveParams } from "./ThreadSectionMoveParams"; +export type { ThreadSectionMoveResponse } from "./ThreadSectionMoveResponse"; +export type { ThreadSectionUpdateParams } from "./ThreadSectionUpdateParams"; +export type { ThreadSectionUpdateResponse } from "./ThreadSectionUpdateResponse"; +export type { ThreadSetNameParams } from "./ThreadSetNameParams"; +export type { ThreadSetNameResponse } from "./ThreadSetNameResponse"; +export type { ThreadSettings } from "./ThreadSettings"; +export type { ThreadSettingsUpdateParams } from "./ThreadSettingsUpdateParams"; +export type { ThreadSettingsUpdateResponse } from "./ThreadSettingsUpdateResponse"; +export type { ThreadSettingsUpdatedNotification } from "./ThreadSettingsUpdatedNotification"; +export type { ThreadShellCommandParams } from "./ThreadShellCommandParams"; +export type { ThreadShellCommandResponse } from "./ThreadShellCommandResponse"; +export type { ThreadSortKey } from "./ThreadSortKey"; +export type { ThreadSource } from "./ThreadSource"; +export type { ThreadSourceKind } from "./ThreadSourceKind"; +export type { ThreadStartParams } from "./ThreadStartParams"; +export type { ThreadStartResponse } from "./ThreadStartResponse"; +export type { ThreadStartSource } from "./ThreadStartSource"; +export type { ThreadStartedNotification } from "./ThreadStartedNotification"; +export type { ThreadStatus } from "./ThreadStatus"; +export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification"; +export type { ThreadTimelineEntry } from "./ThreadTimelineEntry"; +export type { ThreadTimelineListParams } from "./ThreadTimelineListParams"; +export type { ThreadTimelineListResponse } from "./ThreadTimelineListResponse"; +export type { ThreadTokenUsage } from "./ThreadTokenUsage"; +export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification"; +export type { ThreadTurnsListParams } from "./ThreadTurnsListParams"; +export type { ThreadTurnsListResponse } from "./ThreadTurnsListResponse"; +export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams"; +export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse"; +export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification"; +export type { ThreadUnsubscribeParams } from "./ThreadUnsubscribeParams"; +export type { ThreadUnsubscribeResponse } from "./ThreadUnsubscribeResponse"; +export type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus"; +export type { ThreadUsage } from "./ThreadUsage"; +export type { ThreadUsageBreakdownGroup } from "./ThreadUsageBreakdownGroup"; +export type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; +export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer"; +export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption"; +export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams"; +export type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion"; +export type { ToolRequestUserInputResponse } from "./ToolRequestUserInputResponse"; +export type { ToolsV2 } from "./ToolsV2"; +export type { Turn } from "./Turn"; +export type { TurnCompletedNotification } from "./TurnCompletedNotification"; +export type { TurnDiffUpdatedNotification } from "./TurnDiffUpdatedNotification"; +export type { TurnEnvironmentParams } from "./TurnEnvironmentParams"; +export type { TurnError } from "./TurnError"; +export type { TurnInterruptParams } from "./TurnInterruptParams"; +export type { TurnInterruptResponse } from "./TurnInterruptResponse"; +export type { TurnItemsView } from "./TurnItemsView"; +export type { TurnModerationMetadataNotification } from "./TurnModerationMetadataNotification"; +export type { TurnPlanStep } from "./TurnPlanStep"; +export type { TurnPlanStepStatus } from "./TurnPlanStepStatus"; +export type { TurnPlanUpdatedNotification } from "./TurnPlanUpdatedNotification"; +export type { TurnStartParams } from "./TurnStartParams"; +export type { TurnStartResponse } from "./TurnStartResponse"; +export type { TurnStartedNotification } from "./TurnStartedNotification"; +export type { TurnStatus } from "./TurnStatus"; +export type { TurnSteerParams } from "./TurnSteerParams"; +export type { TurnSteerResponse } from "./TurnSteerResponse"; +export type { TurnsPage } from "./TurnsPage"; +export type { UserInput } from "./UserInput"; +export type { WarningNotification } from "./WarningNotification"; +export type { WebSearchAction } from "./WebSearchAction"; +export type { WindowsSandboxReadiness } from "./WindowsSandboxReadiness"; +export type { WindowsSandboxReadinessResponse } from "./WindowsSandboxReadinessResponse"; +export type { WindowsSandboxSetupCompletedNotification } from "./WindowsSandboxSetupCompletedNotification"; +export type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; +export type { WindowsSandboxSetupStartParams } from "./WindowsSandboxSetupStartParams"; +export type { WindowsSandboxSetupStartResponse } from "./WindowsSandboxSetupStartResponse"; +export type { WindowsWorldWritableWarningNotification } from "./WindowsWorldWritableWarningNotification"; +export type { WorkspaceMessage } from "./WorkspaceMessage"; +export type { WorkspaceMessageType } from "./WorkspaceMessageType"; +export type { WriteStatus } from "./WriteStatus"; diff --git a/src/runtimes/openai/private-citation-filter.ts b/src/runtimes/openai/private-citation-filter.ts index a39e798..d1e202a 100644 --- a/src/runtimes/openai/private-citation-filter.ts +++ b/src/runtimes/openai/private-citation-filter.ts @@ -123,15 +123,20 @@ export class OpenAiPrivateCitationStreamFilter { } finish(): OpenAiPrivateCitationFilterResult { + const result = this.previewFinish(); + this.#discardUntilEnd = false; + this.#pendingMarkup = ""; + return result; + } + + previewFinish(): OpenAiPrivateCitationFilterResult { if (this.#discardUntilEnd) { - this.#discardUntilEnd = false; return { privateCitationCount: 0, text: "" }; } const result = scanOpenAiPrivateCitations(this.#pendingMarkup, { preserveIncompleteMarkup: false, }); - this.#pendingMarkup = ""; return { privateCitationCount: result.privateCitationCount, diff --git a/src/runtimes/provider-json.ts b/src/runtimes/provider-json.ts index 88e6378..35d0a74 100644 --- a/src/runtimes/provider-json.ts +++ b/src/runtimes/provider-json.ts @@ -2,6 +2,67 @@ import { formatLogValue } from "../observability"; export type JsonObject = Record; +function jsonStringContentBytes(value: string): number { + return Buffer.byteLength(JSON.stringify(value), "utf8") - 2; +} + +export function chunkJsonText( + text: string, + firstChunkBytes: number, + remainingChunkBytes = firstChunkBytes, +): string[] { + if (text.length === 0 || jsonStringContentBytes(text) <= firstChunkBytes) { + return [text]; + } + + const chunks: string[] = []; + let offset = 0; + let chunkBytes = firstChunkBytes; + + while (offset < text.length) { + let end = offset; + let low = offset + 1; + let high = Math.min(text.length, offset + chunkBytes); + + while (low <= high) { + const probe = Math.floor((low + high) / 2); + let candidate = probe; + const previous = text.charCodeAt(candidate - 1); + const next = text.charCodeAt(candidate); + + if ( + candidate < text.length && + previous >= 0xd800 && + previous <= 0xdbff && + next >= 0xdc00 && + next <= 0xdfff + ) { + candidate -= 1; + } + + if (jsonStringContentBytes(text.slice(offset, candidate)) <= chunkBytes) { + end = candidate; + low = probe + 1; + } else { + high = probe - 1; + } + } + + if (end === offset) { + if (chunks.length > 0) { + throw new RangeError("JSON text chunk byte budget cannot fit the next character."); + } + chunks.push(""); + } else { + chunks.push(text.slice(offset, end)); + offset = end; + } + chunkBytes = remainingChunkBytes; + } + + return chunks; +} + export function isRecord(value: unknown): value is JsonObject { return value !== null && typeof value === "object" && !Array.isArray(value); } @@ -16,6 +77,11 @@ export function readNonEmptyString(value: JsonObject | null, key: string): strin return entry !== null && entry.length > 0 ? entry : null; } +export function readNumber(value: JsonObject | null, key: string): number | null { + const entry = value?.[key]; + return typeof entry === "number" && Number.isFinite(entry) ? entry : null; +} + export function readRecord(value: JsonObject | null, key: string): JsonObject | null { const entry = value?.[key]; return isRecord(entry) ? entry : null; diff --git a/src/runtimes/provider-registry.ts b/src/runtimes/provider-registry.ts index 488af3e..5a70d11 100644 --- a/src/runtimes/provider-registry.ts +++ b/src/runtimes/provider-registry.ts @@ -47,7 +47,7 @@ const PROVIDERS = [ capabilities: [ ...TEXT_TOOL_CAPABILITIES, { id: "native_resume", status: "supported", version: 1 }, - { id: "thinking_stream", status: "unsupported", version: 1 }, + { id: "thinking_stream", status: "supported", version: 1 }, ], createBackend: (payload) => new OpenAiAppServerDriverBackend(payload), id: "openai-app-server", diff --git a/src/skill-package/archive.ts b/src/skill-package/archive.ts index 3396cfc..3625adb 100644 --- a/src/skill-package/archive.ts +++ b/src/skill-package/archive.ts @@ -1,16 +1,13 @@ -import { Unzip, UnzipInflate, zipSync } from "fflate"; -import type { ZipOptions, Zippable } from "fflate"; +import { Unzip, UnzipInflate } from "fflate"; import { SkillPackageError } from "./errors"; import { admitSkillPackagePath, createSkillPackagePathAdmission } from "./path-admission"; import type { AdmittedSkillPackagePath, SkillPackagePathKind } from "./path-admission"; import { rejectUnsupportedArchivePath } from "./path-admission"; -export type SkillEntryKind = "directory" | "file"; - export interface SkillPackageEntry { body: Uint8Array; - entryKind: SkillEntryKind; + entryKind: SkillPackagePathKind; isExecutable: boolean; path: string; } @@ -28,41 +25,12 @@ interface ZipArchiveMetadata { uncompressedSize: number; } -const DEFAULT_ZIP_OPTIONS: ZipOptions = { - level: 6, -}; -const DEFAULT_ZIP_LEVEL = 6; -const FIXED_ZIP_MTIME = new Date("1980-01-01T00:00:00.000Z"); const UNIX_ZIP_OS = 3; -const EXECUTABLE_FILE_MODE = 0o10_0755; -const REGULAR_FILE_MODE = 0o10_0644; -const DIRECTORY_MODE = 0o04_0755; const BYTE_VALUE_COUNT = 0x01_00; const ZIP_EXTERNAL_ATTRIBUTE_MODE_FACTOR = 0x01_00_00; const ZIP_STORED_COMPRESSION = 0; const ZIP_DEFLATE_COMPRESSION = 8; -export function createZipArchive(entries: SkillPackageEntry[]): Uint8Array { - const archive: Zippable = {}; - const admission = createSkillPackagePathAdmission(); - - for (const entry of entries) { - const admitted = admission.admit(entry.path, entry.entryKind); - rejectUnsupportedArchivePath(admitted); - const archivePath = entry.entryKind === "directory" ? `${admitted.path}/` : admitted.path; - - archive[archivePath] = [entry.body, createZipEntryOptions(entry)]; - } - - try { - return zipSync(archive, DEFAULT_ZIP_OPTIONS); - } catch (error) { - throw new SkillPackageError( - error instanceof Error ? error.message : "Skill zip compression failed.", - ); - } -} - export function extractZipArchive( bytes: Uint8Array, options: SkillArchiveExtractOptions = {}, @@ -214,43 +182,6 @@ export function extractZipArchive( }); } -export function looksLikeZipArchive(bytes: Uint8Array): boolean { - if (bytes.byteLength < 4) { - return false; - } - - if (bytes[0] !== 0x50 || bytes[1] !== 0x4b) { - return false; - } - - return ( - (bytes[2] === 0x03 && bytes[3] === 0x04) || - (bytes[2] === 0x05 && bytes[3] === 0x06) || - (bytes[2] === 0x07 && bytes[3] === 0x08) - ); -} - -function createZipEntryOptions(entry: SkillPackageEntry): ZipOptions { - return { - attrs: getZipEntryFileMode(entry) * ZIP_EXTERNAL_ATTRIBUTE_MODE_FACTOR, - level: entry.entryKind === "directory" ? 0 : DEFAULT_ZIP_LEVEL, - mtime: FIXED_ZIP_MTIME, - os: UNIX_ZIP_OS, - }; -} - -function getZipEntryFileMode(entry: SkillPackageEntry): number { - if (entry.entryKind === "directory") { - return DIRECTORY_MODE; - } - - if (entry.isExecutable) { - return EXECUTABLE_FILE_MODE; - } - - return REGULAR_FILE_MODE; -} - function listZipArchiveEntries( bytes: Uint8Array, options: SkillArchiveExtractOptions, @@ -294,10 +225,9 @@ function listZipArchiveEntries( } const rawPath = decodeZipFileName(bytes.subarray(fileNameStart, fileNameEnd)); - const entryKind = inferZipEntryKind(rawPath); - const admitted = admission.admit(rawPath, entryKind); + const admitted = admission.admit(rawPath); rejectUnsupportedArchivePath(admitted); - const path = admitted.path; + const { entryKind, path } = admitted; if (options.maxEntryCount !== undefined && metadata.length >= options.maxEntryCount) { throw new SkillPackageError( @@ -377,7 +307,7 @@ function findEndOfCentralDirectory(bytes: Uint8Array): number { function readAdmittedZipArchivePath(path: string): AdmittedSkillPackagePath | SkillPackageError { try { - const admitted = admitSkillPackagePath(path, inferZipEntryKind(path)); + const admitted = admitSkillPackagePath(path); rejectUnsupportedArchivePath(admitted); return admitted; } catch (error) { @@ -385,10 +315,6 @@ function readAdmittedZipArchivePath(path: string): AdmittedSkillPackagePath | Sk } } -function inferZipEntryKind(path: string): SkillPackagePathKind { - return path.endsWith("/") || path.endsWith("\\") ? "directory" : "file"; -} - function decodeZipFileName(bytes: Uint8Array): string { try { return new TextDecoder("utf-8", { fatal: true }).decode(bytes); diff --git a/src/skill-package/path-admission.ts b/src/skill-package/path-admission.ts index 75fca02..0c76610 100644 --- a/src/skill-package/path-admission.ts +++ b/src/skill-package/path-admission.ts @@ -8,7 +8,7 @@ export interface AdmittedSkillPackagePath { } export interface SkillPackagePathAdmission { - admit(path: string, entryKind: SkillPackagePathKind): AdmittedSkillPackagePath; + admit(path: string, entryKind?: SkillPackagePathKind): AdmittedSkillPackagePath; } export const SKILL_PACKAGE_MANIFEST_PATH = "SKILL.md"; diff --git a/tests/acp-agent-process.test.ts b/tests/acp-agent-process.test.ts index 1d0efcb..9fc64f2 100644 --- a/tests/acp-agent-process.test.ts +++ b/tests/acp-agent-process.test.ts @@ -25,7 +25,10 @@ function createHarness() { sink: async () => {}, }); const context = createAgentDriverContext({ - eventSink: { pushEvents: async () => ({ accepted: [] }) }, + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, logger, payload: driverStartInput, permission: { request: async () => "reject_once" }, diff --git a/tests/acp-client-request-handler.test.ts b/tests/acp-client-request-handler.test.ts index 75dafdc..14eb43f 100644 --- a/tests/acp-client-request-handler.test.ts +++ b/tests/acp-client-request-handler.test.ts @@ -3,11 +3,16 @@ import { describe, expect, test } from "bun:test"; import { DriverPermissionBroker, PermissionEventDeliveryError, - type DriverPermissionRequest, } from "../src/core/driver-permission-broker"; -import type { DriverRuntimeEventPort } from "../src/core/driver-runtime-io"; +import type { DriverPermissionRequest } from "../src/host-ports"; +import { + DriverEventRejectedError, + type DriverRuntimeEventPort, +} from "../src/core/driver-runtime-io"; +import type { DriverEventInput } from "../src/protocol/events"; import { AcpClientRequestHandler } from "../src/runtimes/acp/acp-client-request-handler"; -import { AcpTurnEventState } from "../src/runtimes/acp/acp-event-translator"; +import { AcpAssistantTranscriptState } from "../src/runtimes/acp/acp-assistant-transcript-state"; +import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; describe("ACP client request handler", () => { test("rejects permission requests outside an active turn", async () => { @@ -23,7 +28,7 @@ describe("ACP client request handler", () => { push: async () => { pushes += 1; }, - turnEvents: new AcpTurnEventState(), + turnEvents: new AcpAssistantTranscriptState(), }); const context = { ports: { @@ -50,12 +55,11 @@ describe("ACP client request handler", () => { }); test("closes permission ingress before a turn terminal and reopens it for the next turn", async () => { - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); let permissionRequests = 0; turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -105,7 +109,7 @@ describe("ACP client request handler", () => { push: async (_context, reason, _events) => { pushedReasons.push(reason); }, - turnEvents: new AcpTurnEventState(), + turnEvents: new AcpAssistantTranscriptState(), }); for (const replaying of [false, true]) { @@ -144,7 +148,7 @@ describe("ACP client request handler", () => { push: async (_context, reason) => { pushedReasons.push(reason); }, - turnEvents: new AcpTurnEventState(), + turnEvents: new AcpAssistantTranscriptState(), }); const notification = { sessionId: "native-session-1", @@ -176,7 +180,7 @@ describe("ACP client request handler", () => { nativeSessionId: () => "native-session-1", onUpdateFailure: () => {}, push: async () => {}, - turnEvents: new AcpTurnEventState(), + turnEvents: new AcpAssistantTranscriptState(), }); const context = {} as never; const { terminalId } = await handler.createTerminal(context, { @@ -197,11 +201,10 @@ describe("ACP client request handler", () => { test("serializes official SDK notifications and drains scoped suppression", async () => { const gate = Promise.withResolvers(); const pushedReasons: string[] = []; - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -259,17 +262,16 @@ describe("ACP client request handler", () => { expect(pushedReasons).toEqual(["driver.acp.session.update"]); }); - test("does not hold session update admission behind delivery acknowledgement", async () => { + test("keeps accepting bounded updates while a prior delivery is pending", async () => { const firstDelivery = Promise.withResolvers(); const firstAdmitted = Promise.withResolvers(); const failures: Error[] = []; const pending: Promise[] = []; - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); let pushes = 0; turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -318,11 +320,10 @@ describe("ACP client request handler", () => { test("fails every queued update after the first commit failure", async () => { const failures: Error[] = []; let pushes = 0; - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -362,18 +363,299 @@ describe("ACP client request handler", () => { expect(failures).toHaveLength(1); }); + test("does not translate a queued update before the prior checkpoint settles", async () => { + const firstPublishing = Promise.withResolvers(); + const rejectFirst = Promise.withResolvers(); + const attempts: unknown[] = []; + const turnEvents = new AcpAssistantTranscriptState(); + turnEvents.begin({ + messageId: "message-1" as never, + runId: "run-1" as never, + }); + const createHandler = ( + push: ConstructorParameters[0]["push"], + ) => + new AcpClientRequestHandler({ + allowedRoots: [], + cwd: "/workspace", + env: {}, + isCancelling: () => false, + nativeSessionId: () => "native-session-1", + onUpdateFailure: () => {}, + push, + turnEvents, + }); + const handler = createHandler(async (_context, _reason, events) => { + attempts.push(structuredClone(events)); + firstPublishing.resolve(); + await rejectFirst.promise; + throw new Error("first update rejected"); + }); + const update = (messageId: string, text: string) => ({ + sessionId: "native-session-1", + update: { + content: { text, type: "text" as const }, + messageId, + sessionUpdate: "agent_message_chunk" as const, + }, + }); + const first = handler.enqueueUpdate({} as never, update("native-1", "first")); + const second = handler.enqueueUpdate({} as never, update("native-2", "second")); + void first.catch(() => {}); + void second.catch(() => {}); + + await firstPublishing.promise; + await Promise.resolve(); + expect(attempts).toHaveLength(1); + rejectFirst.resolve(); + await expect(first).rejects.toThrow("first update rejected"); + await expect(second).rejects.toThrow("first update rejected"); + + const replayed: unknown[] = []; + await expect( + createHandler(async (_context, _reason, events) => { + replayed.push(structuredClone(events)); + }).enqueueUpdate({} as never, update("native-2", "second")), + ).resolves.toBeUndefined(); + expect((replayed[0] as Array<{ kind: string }>).map((event) => event.kind)).toEqual([ + "message.started", + "message.delta", + ]); + }); + + test("rolls back a rejected permission tool before translating a queued update", async () => { + const permissionPublishing = Promise.withResolvers(); + const rejectPermission = Promise.withResolvers(); + const updateAttempts: unknown[] = []; + const turnEvents = new AcpAssistantTranscriptState(); + turnEvents.begin({ + messageId: "message-1" as never, + runId: "run-1" as never, + }); + const handler = new AcpClientRequestHandler({ + allowedRoots: [], + cwd: "/workspace", + env: {}, + isCancelling: () => false, + nativeSessionId: () => "native-session-1", + onUpdateFailure: () => {}, + push: async (_context, reason, events) => { + if (reason === "driver.acp.permission.tool") { + permissionPublishing.resolve(); + await rejectPermission.promise; + throw new Error("permission tool rejected"); + } + + updateAttempts.push(structuredClone(events)); + }, + turnEvents, + }); + const context = { + ports: { + permission: { + request: async () => "allow_once" as const, + }, + }, + } as never; + const permission = handler.requestPermission(context, 1, { + options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], + sessionId: "native-session-1", + toolCall: { status: "in_progress", title: "Run command", toolCallId: "tool-1" }, + }); + void permission.catch(() => {}); + + await permissionPublishing.promise; + const update = handler.enqueueUpdate(context, { + sessionId: "native-session-1", + update: { + content: { text: "answer", type: "text" }, + messageId: "native-message-1", + sessionUpdate: "agent_message_chunk", + }, + }); + await Promise.resolve(); + expect(updateAttempts).toHaveLength(0); + + rejectPermission.resolve(); + await expect(permission).rejects.toThrow("permission tool rejected"); + await expect(update).resolves.toBeUndefined(); + expect((updateAttempts[0] as Array<{ kind: string }>).map((event) => event.kind)).toEqual([ + "message.started", + "message.delta", + ]); + }); + + test("commits a tool update after the publisher resumes a retained suffix", async () => { + const attempts: DriverEventInput[][] = []; + let attempt = 0; + let sequence = 0; + const turnEvents = new AcpAssistantTranscriptState(); + turnEvents.begin({ + messageId: "message-1" as never, + runId: "run-1" as never, + }); + const context = { + logger: { debug: () => {} }, + ports: { + eventSink: { + currentRunId: () => "run-1", + pushEvents: async ({ events }: { events: DriverEventInput[] }) => { + attempts.push(structuredClone(events)); + attempt += 1; + + if (attempt === 1) { + return { + accepted: [ + { eventId: events[0]!.sourceEventId, seq: ++sequence, type: events[0]!.kind }, + ], + }; + } + + if (attempt === 2) { + throw new Error("tool update transport interrupted"); + } + + return { + accepted: events.map((event) => ({ + eventId: event.sourceEventId, + seq: ++sequence, + type: event.kind, + })), + }; + }, + }, + }, + } as never; + const publisher = new DriverEventPublisher("acp-fallback", () => "native-session-1"); + const handler = new AcpClientRequestHandler({ + allowedRoots: [], + cwd: "/workspace", + env: {}, + isCancelling: () => false, + nativeSessionId: () => "native-session-1", + onUpdateFailure: () => {}, + push: (pushContext, reason, events) => publisher.push(pushContext, reason, events), + turnEvents, + }); + + await expect( + handler.enqueueUpdate(context, { + sessionId: "native-session-1", + update: { + kind: "execute", + sessionUpdate: "tool_call", + status: "in_progress", + title: "Run command", + toolCallId: "tool-1", + }, + }), + ).resolves.toBeUndefined(); + const replayedSuffix = attempts[2]; + expect(replayedSuffix?.map((event) => event.kind)).toEqual([ + "item.started", + "tool.call.updated", + ]); + + const followupStart = attempts.length; + await expect( + handler.enqueueUpdate(context, { + sessionId: "native-session-1", + update: { + rawOutput: "done", + sessionUpdate: "tool_call_update", + status: "completed", + toolCallId: "tool-1", + }, + }), + ).resolves.toBeUndefined(); + expect( + attempts.slice(followupStart).flatMap((batch) => batch.map((event) => event.kind)), + ).toEqual(["tool.call.updated", "item.completed"]); + }); + + test("rolls back an unchanged tool update after an explicit sink rejection", async () => { + const attempts: DriverEventInput[][] = []; + let reject = true; + let sequence = 0; + const turnEvents = new AcpAssistantTranscriptState(); + turnEvents.begin({ + messageId: "message-1" as never, + runId: "run-1" as never, + }); + const context = { + logger: { debug: () => {} }, + ports: { + eventSink: { + currentRunId: () => "run-1", + pushEvents: async ({ events }: { events: DriverEventInput[] }) => { + attempts.push(structuredClone(events)); + if (reject) { + throw new DriverEventRejectedError( + events[0]!.sourceEventId!, + new Error("tool update rejected"), + ); + } + + return { + accepted: events.map((event) => ({ + eventId: event.sourceEventId, + seq: ++sequence, + type: event.kind, + })), + }; + }, + }, + }, + } as never; + const publisher = new DriverEventPublisher("acp-fallback", () => "native-session-1"); + const createHandler = () => + new AcpClientRequestHandler({ + allowedRoots: [], + cwd: "/workspace", + env: {}, + isCancelling: () => false, + nativeSessionId: () => "native-session-1", + onUpdateFailure: () => {}, + push: (pushContext, reason, events) => publisher.push(pushContext, reason, events), + turnEvents, + }); + const notification = { + sessionId: "native-session-1", + update: { + kind: "execute" as const, + sessionUpdate: "tool_call" as const, + status: "in_progress" as const, + title: "Run command", + toolCallId: "tool-1", + }, + }; + + await expect(createHandler().enqueueUpdate(context, notification)).rejects.toThrow( + "tool update rejected", + ); + const firstToolEventId = attempts[0]!.at(-1)!.sourceEventId; + reject = false; + await expect(createHandler().enqueueUpdate(context, notification)).resolves.toBeUndefined(); + + expect(attempts.at(-1)!.map((event) => event.kind)).toEqual([ + "message.started", + "item.started", + "tool.call.updated", + ]); + expect(attempts.at(-1)!.at(-1)!.sourceEventId).toBe(firstToolEventId); + }); + test.each(["update drain", "tool event"] as const)( "cancels a permission request while waiting on the %s boundary", async (boundary) => { const blocked = Promise.withResolvers(); const release = Promise.withResolvers(); - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); let cancelling = false; let permissionRequests = 0; turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const blockedReason = boundary === "update drain" ? "driver.acp.session.update" : "driver.acp.permission.tool"; @@ -432,12 +714,11 @@ describe("ACP client request handler", () => { test("rejects a permission when its turn ends during the update drain", async () => { const updatePublishing = Promise.withResolvers(); const releaseUpdate = Promise.withResolvers(); - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); let permissionRequests = 0; turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -490,12 +771,11 @@ describe("ACP client request handler", () => { test("leaves permission lifecycle events to the host port and keeps typed RPC IDs distinct", async () => { const eventKinds: string[] = []; - const requestIds: string[] = []; - const turnEvents = new AcpTurnEventState(); + const requests: DriverPermissionRequest[] = []; + const turnEvents = new AcpAssistantTranscriptState(); turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -512,8 +792,8 @@ describe("ACP client request handler", () => { const context = { ports: { permission: { - request: async ({ requestId }: { requestId: string }) => { - requestIds.push(requestId); + request: async (request: DriverPermissionRequest) => { + requests.push(request); return "allow_once" as const; }, }, @@ -530,7 +810,14 @@ describe("ACP client request handler", () => { ).resolves.toEqual({ outcome: { optionId: "allow", outcome: "selected" } }); } - expect(requestIds).toEqual(["number:1", "string:1", "null"]); + expect(requests.map(({ requestId }) => requestId)).toEqual(["number:1", "string:1", "null"]); + expect(requests[0]).toEqual({ + rawInput: "", + requestId: "number:1", + title: "Run command", + toolCallId: "tool-0", + toolKind: null, + }); expect(eventKinds).not.toContain("permission.requested"); expect(eventKinds).not.toContain("permission.resolved"); }); @@ -543,11 +830,10 @@ describe("ACP client request handler", () => { ] as const)( "maps a %s host decision to a %s option without widening scope", async (decision, optionKind, optionId, selectable) => { - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -580,11 +866,10 @@ describe("ACP client request handler", () => { test("returns cancelled when the SDK aborts a pending permission request", async () => { const entered = Promise.withResolvers(); const controller = new AbortController(); - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -628,12 +913,11 @@ describe("ACP client request handler", () => { const toolPublishing = Promise.withResolvers(); const releaseTool = Promise.withResolvers(); const controller = new AbortController(); - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); let permissionRequests = 0; turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -694,11 +978,10 @@ describe("ACP client request handler", () => { "resolved", new Error("permission transport unavailable"), ); - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -763,11 +1046,10 @@ describe("ACP client request handler", () => { const broker = new DriverPermissionBroker(() => null, { eventDeliveryTimeoutMs: 1_000, }); - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); turnEvents.begin({ messageId: "message-1" as never, runId: activeRunId as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], @@ -820,11 +1102,10 @@ describe("ACP client request handler", () => { test("closes update ingress and drains accepted work before stopping", async () => { const blocked = Promise.withResolvers(); const release = Promise.withResolvers(); - const turnEvents = new AcpTurnEventState(); + const turnEvents = new AcpAssistantTranscriptState(); turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never, - sessionId: "native-session-1", }); const handler = new AcpClientRequestHandler({ allowedRoots: [], diff --git a/tests/acp-configuration.test.ts b/tests/acp-configuration.test.ts index dcde739..3aab0ca 100644 --- a/tests/acp-configuration.test.ts +++ b/tests/acp-configuration.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"; import { createBufferedSinkLogger } from "../src/observability"; import { @@ -81,6 +82,98 @@ describe("ACP runtime configuration", () => { ); }); + test("leaves JSON-RPC parsing and wire validation to the official SDK", async () => { + let sdkOutput = ""; + const inputReady = Promise.withResolvers>(); + const handled = Promise.withResolvers(); + const input = new ReadableStream({ + start(controller) { + inputReady.resolve(controller); + controller.enqueue( + new TextEncoder().encode( + '{\n{}\n{"jsonrpc":"2.0","id":1,"method":"fs/read_text_file","params":{"sessionId":"session-1","path":"/tmp/alive"}}\n', + ), + ); + }, + }); + const transport = ndJsonStream( + new WritableStream({ + write(chunk) { + sdkOutput += new TextDecoder().decode(chunk); + if (sdkOutput.includes('"content":"alive"')) handled.resolve(); + }, + }), + limitAcpInput(input), + ); + const connection = new ClientSideConnection( + () => ({ + readTextFile: async () => ({ content: "alive" }), + requestPermission: async () => ({ outcome: { outcome: "cancelled" } }), + sessionUpdate: async () => {}, + }), + transport, + ); + + await handled.promise; + (await inputReady.promise).close(); + void connection; + expect( + sdkOutput + .trim() + .split("\n") + .map((line) => JSON.parse(line)), + ).toMatchObject([ + { error: { code: -32700 }, id: null, jsonrpc: "2.0" }, + { error: { code: -32600 }, id: null, jsonrpc: "2.0" }, + { id: 1, jsonrpc: "2.0", result: { content: "alive" } }, + ]); + }); + + test("counts UTF-8 bytes across chunk boundaries", async () => { + const bytes = new TextEncoder().encode("😀\n"); + const stream = (limit: number) => + limitAcpInput( + new ReadableStream({ + start(controller) { + controller.enqueue(bytes.subarray(0, 3)); + controller.enqueue(bytes.subarray(3)); + controller.close(); + }, + }), + limit, + ); + + await expect(new Response(stream(3)).text()).rejects.toThrow("message exceeds 3 bytes"); + await expect(new Response(stream(4)).text()).resolves.toBe("😀\n"); + }); + + test("rejects malformed UTF-8 before the ACP decoder", async () => { + const input = new ReadableStream({ + start(controller) { + controller.enqueue(Uint8Array.of(0xc3, 0x28, 0x0a)); + controller.close(); + }, + }); + + await expect(new Response(limitAcpInput(input)).text()).rejects.toThrow(); + }); + + test("passes one JSON-RPC object across UTF-8 chunks and an unterminated EOF line", async () => { + const message = '{"jsonrpc":"2.0","method":"session/update","params":{"text":"😀"}}'; + const bytes = new TextEncoder().encode(`\n${message}`); + const emoji = new TextEncoder().encode("😀"); + const split = bytes.findIndex((byte) => byte === emoji[0]) + 2; + const input = new ReadableStream({ + start(controller) { + controller.enqueue(bytes.subarray(0, split)); + controller.enqueue(bytes.subarray(split)); + controller.close(); + }, + }); + + expect(await new Response(limitAcpInput(input)).text()).toBe(`\n${message}`); + }); + test("advertises stable boolean configuration support", () => { expect(buildClientCapabilities()).toEqual({ fs: { @@ -115,18 +208,32 @@ describe("ACP runtime configuration", () => { test("fails fast when a configured auth method is not advertised", () => { expect( - resolveAuthMethod([{ id: "browser-login" }], { + resolveAuthMethod([{ id: "browser-login", name: "Browser login" }], { MOSOO_ACP_AUTH_METHOD_ID: "browser-login", }), ).toBe("browser-login"); - expect(resolveAuthMethod([{ id: "browser-login" }], {})).toBeNull(); + expect(resolveAuthMethod([{ id: "browser-login", name: "Browser login" }], {})).toBeNull(); expect(() => - resolveAuthMethod([{ id: "browser-login" }], { + resolveAuthMethod([{ id: "browser-login", name: "Browser login" }], { MOSOO_ACP_AUTH_METHOD_ID: "device-login", }), ).toThrow(); + + expect(() => + resolveAuthMethod( + [ + { + args: ["auth"], + id: "terminal-login", + name: "Terminal login", + type: "terminal", + }, + ], + { MOSOO_ACP_AUTH_METHOD_ID: "terminal-login" }, + ), + ).toThrow("requires unsupported terminal auth"); }); test("inherits only runtime proxy env and prepends artifact paths", () => { @@ -195,6 +302,7 @@ describe("ACP runtime configuration", () => { const context = createAgentDriverContext({ eventSink: { commandUpdate: async () => {}, + currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, logger, diff --git a/tests/acp-driver-backend.test.ts b/tests/acp-driver-backend.test.ts index 9525a97..e09a950 100644 --- a/tests/acp-driver-backend.test.ts +++ b/tests/acp-driver-backend.test.ts @@ -223,6 +223,60 @@ const handle = (message) => { }); return; } + if (message.params.prompt[0]?.text === "many-tools") { + for (let index = 0; index < 32; index += 1) { + send({ + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId: "native-session-1", + update: { + kind: "execute", + sessionUpdate: "tool_call", + status: "in_progress", + title: "Tool " + index, + toolCallId: "tool-" + index, + }, + }, + }); + } + result = { stopReason: "end_turn" }; + break; + } + if (message.params.prompt[0]?.text === "sticky-update-failure") { + send({ + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId: "native-session-1", + update: { + kind: "execute", + sessionUpdate: "tool_call", + status: "in_progress", + title: "Fail delivery", + toolCallId: "tool-sticky", + }, + }, + }); + result = { stopReason: "end_turn" }; + break; + } + if (message.params.prompt[0]?.text === "thought-only") { + send({ + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId: "native-session-1", + update: { + content: { text: "final from thought", type: "text" }, + messageId: "assistant-thought", + sessionUpdate: "agent_thought_chunk", + }, + }, + }); + result = { stopReason: "end_turn" }; + break; + } const chunks = message.params.prompt[0]?.text === "burst" ? ["one", "two", "three"] : ["done"]; for (const text of chunks) { @@ -391,10 +445,17 @@ async function createHarness( readonly kind: string; readonly release: ReturnType>; } | null = null; + let activeRunId: RunId | null = null; let failKind: string | null = null; const context = createAgentDriverContext({ eventSink: { + currentRunId: () => activeRunId, pushEvents: async ({ events }) => { + const runStarted = events.find((event) => event.kind === "run.started"); + if (runStarted !== undefined) { + activeRunId = runStarted.runId ?? null; + } + if (failKind !== null && events.some((event) => event.kind === failKind)) { failKind = null; throw new Error("event sink unavailable"); @@ -409,6 +470,15 @@ async function createHarness( } publishedEvents.push(...events); + for (const event of events) { + if ( + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed" + ) { + activeRunId = null; + } + } options.onEvents?.(events); return { @@ -550,6 +620,30 @@ describe("ACP driver backend lifecycle", () => { } }); + test("publishes a thought-only fallback through lossless terminal closures", async () => { + const harness = await createHarness(); + + try { + await harness.backend.handleInput( + harness.context, + { text: "thought-only" }, + DRIVER_TEST_IDS.runId as RunId, + ); + + expect( + harness.events.find( + (event) => + event.kind === "message.added" && + (event.payload as Record)["role"] === "agent", + )?.payload, + ).toMatchObject({ content: "final from thought" }); + expect(harness.events.filter((event) => event.kind === "run.completed")).toHaveLength(1); + expect(harness.lifecycleFailures).toEqual([]); + } finally { + await harness.destroy(); + } + }); + test("clears turn state when prompt-start publication fails", async () => { const harness = await createHarness(); @@ -841,6 +935,63 @@ describe("ACP driver backend lifecycle", () => { } }); + test("restores an explicitly rejected terminal settlement for authoritative replay", async () => { + const harness = await createHarness(); + let rejectedSettlement: DriverEventInput[] | null = null; + const turn = new AcpTurnController( + async () => {}, + async () => {}, + async (_context, _reason, closures, terminal) => { + rejectedSettlement = structuredClone([...closures, terminal]); + throw new Error("terminal settlement rejected"); + }, + ); + const clientRequests = new AcpClientRequestHandler({ + allowedRoots: [process.cwd()], + cwd: process.cwd(), + env: {}, + isCancelling: () => turn.isCancelling(), + nativeSessionId: () => "native-session-1", + onUpdateFailure: () => {}, + push: async () => {}, + turnEvents: turn.events, + }); + const connection = { + notify: async () => {}, + request: async () => { + await clientRequests.enqueueUpdate(harness.context, { + sessionId: "native-session-1", + update: { + content: { text: "authoritative answer", type: "text" }, + messageId: "native-final", + sessionUpdate: "agent_message_chunk", + }, + }); + return { stopReason: "end_turn" }; + }, + } as unknown as ClientContext; + + try { + await expect( + turn.handleInput( + harness.context, + { text: "complete" }, + DRIVER_TEST_IDS.runId as RunId, + connection, + "native-session-1", + createDriverHostIntegrationSnapshotFromBootExecution(driverBootPayload.execution), + clientRequests, + ), + ).rejects.toThrow("terminal settlement rejected"); + + expect(turn.events.activeRunId()).toBe(DRIVER_TEST_IDS.runId); + expect(turn.events.completePrompt("end_turn", null)).toEqual(rejectedSettlement); + } finally { + turn.events.clear(); + await harness.destroy(); + } + }); + test("does not hold cancellation acknowledgement behind provider recycle", async () => { const harness = await createHarness(); const promptRequested = Promise.withResolvers(); @@ -1315,6 +1466,66 @@ describe("ACP driver backend lifecycle", () => { } }); + test("closes 32 open tools before publishing the run terminal", async () => { + const order: DriverEventInput[] = []; + const harness = await createHarness({ onEvents: (events) => order.push(...events) }); + + try { + await expect( + harness.backend.handleInput( + harness.context, + { text: "many-tools" }, + DRIVER_TEST_IDS.runId as RunId, + ), + ).resolves.toBeUndefined(); + + const completedTools = order.filter( + (event) => + event.kind === "tool.call.updated" && + (event.payload as { readonly status?: unknown }).status === "completed", + ); + const completedItems = order.filter( + (event) => + event.kind === "item.completed" && + (event.payload as { readonly status?: unknown }).status === "completed", + ); + const terminalIndex = order.findIndex((event) => event.kind === "run.completed"); + + expect(completedTools).toHaveLength(32); + expect(completedItems).toHaveLength(32); + expect(terminalIndex).toBe(order.length - 1); + expect(harness.lifecycleFailures).toEqual([]); + } finally { + await harness.destroy(); + } + }); + + test("publishes one failed terminal after a sticky update inbox failure", async () => { + const harness = await createHarness(); + + try { + harness.failNext("item.started"); + await expect( + harness.backend.handleInput( + harness.context, + { text: "sticky-update-failure" }, + DRIVER_TEST_IDS.runId as RunId, + ), + ).rejects.toThrow("event sink unavailable"); + + const terminals = harness.events.filter( + (event) => + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed", + ); + expect(terminals).toHaveLength(1); + expect(terminals[0]?.kind).toBe("run.failed"); + } finally { + await harness.destroy(); + } + }); + test("returns ACP request-cancelled for a nested terminal RPC when the turn is cancelled", async () => { const harness = await createHarness(); @@ -1393,8 +1604,8 @@ describe("ACP driver backend lifecycle", () => { })), ).toEqual([ { kind: "tool.call.updated", status: "running" }, - { kind: "tool.call.updated", status: "failed" }, - { kind: "item.completed", status: "failed" }, + { kind: "tool.call.updated", status: "cancelled" }, + { kind: "item.completed", status: "cancelled" }, { kind: "run.cancelled", status: undefined }, ]); } finally { diff --git a/tests/acp-event-translator-lifecycle.test.ts b/tests/acp-event-translator-lifecycle.test.ts index 8d9a723..9e1c5a5 100644 --- a/tests/acp-event-translator-lifecycle.test.ts +++ b/tests/acp-event-translator-lifecycle.test.ts @@ -1,14 +1,30 @@ import { describe, expect, test } from "bun:test"; +import { RequestError } from "@agentclientprotocol/sdk"; +import { + zAgentResponse, + zPromptRequest, + zSessionNotification, +} from "../node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js"; +import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; +import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; -import type { RunId } from "../src/protocol/id"; +import { createDriverId } from "../src/protocol/id"; +import type { EventId, RunId, SessionId } from "../src/protocol/id"; +import { toRuntimeEventInput } from "../src/runtime-events"; +import { AcpAssistantTranscriptState } from "../src/runtimes/acp/acp-assistant-transcript-state"; +import { limitAcpInput } from "../src/runtimes/acp/acp-driver-backend"; +import { toPermissionRequest } from "../src/runtimes/acp/acp-permission-events"; +import { toPromptStartEvents } from "../src/runtimes/acp/acp-session-events"; +import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; +import { CMA_MAX_EVENT_BYTES, encodeCmaSseRecord } from "../src/stores/cma-store"; +import { createCmaMemoryStore } from "../src/stores/memory"; import { - AcpTurnEventState, - toPermissionRequest, - toPermissionResolvedEvent, -} from "../src/runtimes/acp/acp-event-translator"; -import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; + DRIVER_TEST_IDS, + driverBootPayload, + driverStartInput, +} from "./driver-boot-payload-fixture"; const RUN_ID = "run-1" as RunId; const SECOND_RUN_ID = "run-2" as RunId; @@ -43,13 +59,229 @@ function requireEvent(events: readonly DriverEventInput[], kind: string): Driver } describe("ACP runtime event translation", () => { + test("bounds an official ACP JSON-RPC failure across every terminal event", async () => { + const originalMessage = "x".repeat(1_100_000); + const wire = `${JSON.stringify({ + error: { code: -32_603, message: originalMessage }, + id: 1, + jsonrpc: "2.0", + })}\n`; + const decoded = JSON.parse( + await new Response(limitAcpInput(new Blob([wire]).stream())).text(), + ) as unknown; + const response = zAgentResponse.parse(decoded); + + if (!("error" in response)) { + throw new Error("Expected an official ACP error response."); + } + + const requestError = new RequestError( + response.error.code, + response.error.message, + response.error.data, + ); + const state = new AcpAssistantTranscriptState(); + state.begin({ + messageId: "message-1", + runId: DRIVER_TEST_IDS.runId, + }); + state.translateUpdate({ + update: { + content: { text: "partial", type: "text" }, + messageId: "native-message-1", + sessionUpdate: "agent_message_chunk", + }, + }); + state.translateUpdate({ + update: { + content: { text: "thought", type: "text" }, + messageId: "native-message-1", + sessionUpdate: "agent_thought_chunk", + }, + }); + state.translateUpdate({ + update: { + sessionUpdate: "tool_call", + status: "in_progress", + title: "Run command", + toolCallId: "tool-1", + }, + }); + const events = state.failPrompt({ code: "acp.turn_failed", message: requestError.message }); + const terminal = events.at(-1)!; + const closures = events.slice(0, -1); + const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); + const canonicalEvents: DriverEventInput[] = []; + let cmaRecordCount = 0; + const logger = createBufferedSinkLogger({ + level: "debug", + service: "acp-terminal-boundary-test", + sink: async () => {}, + }); + let sequence = 0; + const context = createAgentDriverContext({ + eventSink: { + currentRunId: () => DRIVER_TEST_IDS.runId, + pushEvents: async ({ events: drafts }) => { + const envelopes = drafts.flatMap((draft) => + toDriverEventEnvelopes(driverBootPayload, draft, DRIVER_TEST_IDS.runId), + ); + + for (const envelope of envelopes) { + canonicalEvents.push(envelope.event); + cmaRecordCount += ( + await store.appendDriverEvent(DRIVER_TEST_IDS.sessionId, envelope.event) + ).length; + } + + return { + accepted: envelopes.map((envelope) => ({ + eventId: envelope.eventId, + seq: ++sequence, + type: envelope.event.kind, + })), + }; + }, + }, + logger, + payload: driverStartInput, + permission: { request: async () => "reject_once" }, + }); + + try { + await new DriverEventPublisher("acp-fallback", () => "native-session-1").pushTerminal( + context, + "driver.acp.prompt.failed", + closures, + terminal, + ); + } finally { + await logger.destroy(); + } + + const boundedMessage = + "ACP failure exceeded durable event capacity (originalMessageUtf8Bytes=1100000)."; + const closureMessages = events.flatMap((event): string[] => { + const payload = eventPayload(event); + const error = payload["error"]; + + if (typeof error === "string") { + return [error]; + } + if (typeof error === "object" && error !== null && "message" in error) { + return [String(error.message)]; + } + return typeof payload["reason"] === "string" ? [payload["reason"]] : []; + }); + const runError = eventPayload(terminal)["error"] as Record; + + expect(new Set(closureMessages)).toEqual(new Set([boundedMessage])); + expect(runError["details"]).toEqual({ originalMessageUtf8Bytes: 1_100_000 }); + expect(canonicalEvents).toHaveLength(5); + expect(cmaRecordCount).toBe(4); + expect(canonicalEvents.map((event) => event.kind).at(-1)).toBe("run.failed"); + expect( + canonicalEvents.every( + (event) => Buffer.byteLength(JSON.stringify(event), "utf8") < CMA_MAX_EVENT_BYTES, + ), + ).toBe(true); + expect(JSON.stringify(canonicalEvents)).not.toContain(originalMessage); + }); + + test("bounds official ACP prompt text before provider dispatch", async () => { + const parseText = (text: string): string => { + const prompt = zPromptRequest.parse({ + prompt: [{ text, type: "text" }], + sessionId: "native-session-1", + }).prompt[0]; + + return prompt?.type === "text" ? prompt.text : ""; + }; + + expect(() => + toPromptStartEvents({ + messageId: "message-1", + runId: DRIVER_TEST_IDS.runId, + text: parseText("x".repeat(1_100_000)), + }), + ).toThrow("ACP message.added event exceeds 524288 UTF-8 bytes"); + + const events = toPromptStartEvents({ + messageId: "message-1", + runId: DRIVER_TEST_IDS.runId, + text: parseText("x".repeat(500_000)), + }); + const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); + + for (const event of events) { + for (const { event: envelope } of toDriverEventEnvelopes( + driverBootPayload, + event, + DRIVER_TEST_IDS.runId, + )) { + await expect( + store.appendDriverEvent(DRIVER_TEST_IDS.sessionId, envelope), + ).resolves.toBeArray(); + } + } + }); + + test("normalizes official ACP empty chunks and tool titles before canonical ingress", () => { + const state = new AcpAssistantTranscriptState(); + state.begin({ + messageId: "message-1", + runId: DRIVER_TEST_IDS.runId, + }); + + for (const sessionUpdate of ["agent_message_chunk", "agent_thought_chunk"] as const) { + const notification = zSessionNotification.parse({ + sessionId: "native-session-1", + update: { content: { text: "", type: "text" }, sessionUpdate }, + }); + expect(state.translateUpdate(notification)).toEqual([]); + } + + const message = state.translateUpdate( + zSessionNotification.parse({ + sessionId: "native-session-1", + update: { + content: { text: "ok", type: "text" }, + sessionUpdate: "agent_message_chunk", + }, + }), + ); + expect(eventKinds(message)).toEqual(["message.started", "message.delta"]); + expect(message[1]?.sourceEventId).toBe(`acp:${DRIVER_TEST_IDS.runId}:agent-message:1`); + + for (const sessionUpdate of ["tool_call", "tool_call_update"] as const) { + const notification = zSessionNotification.parse({ + sessionId: "native-session-1", + update: { + sessionUpdate, + title: "", + toolCallId: sessionUpdate, + ...(sessionUpdate === "tool_call" ? { kind: "execute" } : {}), + }, + }); + const events = state.translateUpdate(notification); + const update = requireEvent(events, "tool.call.updated"); + + expect(eventPayload(requireEvent(events, "item.started"))["title"]).toBe( + sessionUpdate === "tool_call" ? "execute" : "tool", + ); + expect(eventPayload(update)).not.toHaveProperty("title"); + expect(() => + toDriverEventEnvelopes(driverBootPayload, update, DRIVER_TEST_IDS.runId), + ).not.toThrow(); + } + }); + test("keeps native assistant messages separate across tools and projects only the final one", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "prompt-message-1", runId: RUN_ID, - sessionId: "session-1", }); const progressOne = "进度 1:正在读取上游报告。"; @@ -139,23 +371,84 @@ describe("ACP runtime event translation", () => { expect(eventPayload(toolStarted)).toMatchObject({ parentMessageId: progressOneId, }); - expect(eventPayload(completed)).toMatchObject({ - finalMessageId, - finalMessageText: finalText, + expect(eventPayload(completed)).toEqual({ finalMessageId, stopReason: "end_turn" }); + expect( + events.find( + (event) => + event.kind === "message.added" && eventPayload(event)["messageId"] === finalMessageId, + )?.payload, + ).toMatchObject({ content: finalText }); + }); + + test("materializes oversized final text as bounded lossless CMA events", async () => { + const state = new AcpAssistantTranscriptState(); + const runId = createDriverId() as RunId; + const sessionId = createDriverId() as SessionId; + const text = `开始😀${'"\n\\'.repeat(400_000)}结束`; + + state.begin({ messageId: createDriverId(), runId }); + const streamed = state.translateUpdate({ + update: { + content: { text, type: "text" }, + messageId: "native-final", + sessionUpdate: "agent_message_chunk", + }, }); - expect(eventPayloadString(completed, "finalMessageText")).not.toContain(progressOne); - expect(new TextEncoder().encode(eventPayloadString(completed, "finalMessageText"))).toEqual( - new TextEncoder().encode(finalText), + const terminal = state.completePrompt("end_turn", null); + const snapshots = terminal.filter( + (event) => event.kind === "message.added" || event.kind === "message.delta", ); + const store = createCmaMemoryStore({ sessions: [{ id: sessionId }] }); + const records = []; + + expect(Buffer.byteLength(JSON.stringify(streamed.at(-1)), "utf8")).toBeGreaterThan( + CMA_MAX_EVENT_BYTES, + ); + expect(snapshots.length).toBeGreaterThan(1); + expect(snapshots.every((event) => event.delivery === "lossless")).toBe(true); + + for (const event of terminal) { + const [envelope] = toRuntimeEventInput( + { + createId: () => createDriverId() as EventId, + occurredAt: "2026-08-13T00:00:00.000Z", + runId, + sessionId, + }, + event, + ); + records.push(...(await store.appendDriverEvent(sessionId, envelope!))); + } + + expect( + records.every((record) => encodeCmaSseRecord(record).byteLength < CMA_MAX_EVENT_BYTES), + ).toBe(true); + expect( + records + .flatMap(({ event }) => { + if ( + !("sourceEventKind" in event) || + !["message.added", "message.delta"].includes(event.sourceEventKind) || + typeof event.message !== "object" || + event.message === null + ) { + return []; + } + + const message = event.message as Record; + return [String(message["content"] ?? message["contentDelta"] ?? "")]; + }) + .join(""), + ).toBe(text); + expect(requireEvent(terminal, "run.completed").payload).not.toHaveProperty("finalMessageText"); }); test("uses a later identified final after anonymous progress, but fails closed for an anonymous final", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "prompt-message-1", runId: RUN_ID, - sessionId: "session-1", }); const identifiedFinalText = "最终回答:native identity 使它可安全成为 canonical final。"; @@ -185,17 +478,20 @@ describe("ACP runtime event translation", () => { "message.delta", ); - expect(eventPayload(completed)).toMatchObject({ - finalMessageId: eventPayloadString(finalDelta, "messageId"), - finalMessageText: identifiedFinalText, - }); + const finalMessageId = eventPayloadString(finalDelta, "messageId"); + expect(eventPayload(completed)).toEqual({ finalMessageId, stopReason: "end_turn" }); + expect( + events.find( + (event) => + event.kind === "message.added" && eventPayload(event)["messageId"] === finalMessageId, + )?.payload, + ).toMatchObject({ content: identifiedFinalText }); - const anonymousFinalState = new AcpTurnEventState(); + const anonymousFinalState = new AcpAssistantTranscriptState(); anonymousFinalState.begin({ messageId: "prompt-message-2", runId: SECOND_RUN_ID, - sessionId: "session-1", }); const anonymousFinalEvents = [ ...anonymousFinalState.translateUpdate({ @@ -250,12 +546,11 @@ describe("ACP runtime event translation", () => { }); test("maps ACP turn updates onto canonical runtime events with one tool lifecycle", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); const events = [ @@ -306,6 +601,7 @@ describe("ACP runtime event translation", () => { "tool.call.updated", "tool.call.updated", "item.completed", + "message.added", "message.completed", "usage.updated", "run.completed", @@ -316,8 +612,8 @@ describe("ACP runtime event translation", () => { }); test("projects an execute tool with a nonzero raw exit as failed", () => { - const state = new AcpTurnEventState(); - state.begin({ messageId: "message-1", runId: RUN_ID, sessionId: "session-1" }); + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); state.translateUpdate({ update: { kind: "execute", @@ -346,14 +642,84 @@ describe("ACP runtime event translation", () => { }); expect(eventPayload(requireEvent(events, "item.completed"))).toMatchObject({ itemId: "tool-1", - result: { metadata: { exit: 7 } }, status: "failed", }); + expect(eventPayload(requireEvent(events, "item.completed"))).not.toHaveProperty("result"); + }); + + test("deduplicates identical ACP tool content and fails closed on oversized output", () => { + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); + state.translateUpdate({ + update: { + kind: "execute", + sessionUpdate: "tool_call", + status: "in_progress", + toolCallId: "tool-1", + }, + }); + + expect(() => + state.translateUpdate({ + update: { + rawOutput: "x".repeat(600_000), + sessionUpdate: "tool_call_update", + status: "completed", + toolCallId: "tool-1", + }, + }), + ).toThrow("ACP tool update exceeds 524288 UTF-8 bytes"); + + const events = state.translateUpdate({ + update: { + content: { text: "done", type: "text" }, + rawOutput: "done", + sessionUpdate: "tool_call_update", + status: "completed", + toolCallId: "tool-1", + }, + }); + const update = eventPayload(requireEvent(events, "tool.call.updated")); + + expect(update).toMatchObject({ content: "done", status: "completed" }); + expect(update).not.toHaveProperty("rawOutput"); + expect(eventPayload(requireEvent(events, "item.completed"))).not.toHaveProperty("result"); + }); + + test("fails closed on an oversized permission tool payload", () => { + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); + + expect(() => + state.translatePermission({ + params: { + options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], + toolCall: { + rawInput: "x".repeat(600_000), + status: "in_progress", + toolCallId: "tool-1", + }, + }, + requestId: "request-1", + }), + ).toThrow("ACP tool update exceeds 524288 UTF-8 bytes"); + + expect( + state + .translatePermission({ + params: { + options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], + toolCall: { status: "in_progress", toolCallId: "tool-1" }, + }, + requestId: "request-1", + }) + .events.map((event) => event.kind), + ).toEqual(["message.started", "item.started", "tool.call.updated"]); }); test("keeps a nonzero execute exit across partial updates", () => { - const state = new AcpTurnEventState(); - state.begin({ messageId: "message-1", runId: RUN_ID, sessionId: "session-1" }); + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); state.translateUpdate({ update: { kind: "execute", @@ -394,12 +760,13 @@ describe("ACP runtime event translation", () => { }); test("does not reset tool identity fields omitted by a partial update", () => { - const state = new AcpTurnEventState(); - state.begin({ messageId: "message-1", runId: RUN_ID, sessionId: "session-1" }); + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); const started = state.translateUpdate({ update: { kind: "shell", + name: "Bash", sessionUpdate: "tool_call", status: "running", title: "Run command", @@ -408,6 +775,7 @@ describe("ACP runtime event translation", () => { }); const patched = state.translateUpdate({ update: { + name: "Shell", rawOutput: { text: "done" }, sessionUpdate: "tool_call_update", status: "completed", @@ -417,8 +785,8 @@ describe("ACP runtime event translation", () => { const initialPayload = eventPayload(requireEvent(started, "tool.call.updated")); const patchPayload = eventPayload(requireEvent(patched, "tool.call.updated")); - expect(initialPayload).toMatchObject({ kind: "shell", title: "Run command" }); - expect(patchPayload).toMatchObject({ kind: "shell", title: "Run command" }); + expect(initialPayload).toMatchObject({ kind: "shell", name: "Bash", title: "Run command" }); + expect(patchPayload).toMatchObject({ kind: "shell", name: "Shell", title: "Run command" }); }); test.each([ @@ -437,8 +805,8 @@ describe("ACP runtime event translation", () => { ] as const)( "keeps a terminal tool completed while merging a later %s patch", (_name, patch, expected) => { - const state = new AcpTurnEventState(); - state.begin({ messageId: "message-1", runId: RUN_ID, sessionId: "session-1" }); + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); const initial = state.translateUpdate({ update: { kind: "shell", @@ -494,8 +862,8 @@ describe("ACP runtime event translation", () => { ] as const)( "keeps the first %s status across a late %s update %s", (initialStatus, lateStatus, contentCase) => { - const state = new AcpTurnEventState(); - state.begin({ messageId: "message-1", runId: RUN_ID, sessionId: "session-1" }); + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); state.translateUpdate({ update: { sessionUpdate: "tool_call", @@ -530,8 +898,8 @@ describe("ACP runtime event translation", () => { ); test("emits an empty plan as a full replacement", () => { - const state = new AcpTurnEventState(); - state.begin({ messageId: "message-1", runId: RUN_ID, sessionId: "session-1" }); + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); expect( state.translateUpdate({ @@ -546,12 +914,11 @@ describe("ACP runtime event translation", () => { }); test("omits empty ACP tool input before runtime event ingress", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: DRIVER_TEST_IDS.runId, - sessionId: DRIVER_TEST_IDS.sessionId, }); const events = state.translateUpdate({ @@ -586,7 +953,7 @@ describe("ACP runtime event translation", () => { expect(eventPayload(canonicalToolEvent as DriverEventInput)).not.toHaveProperty("rawInput"); }); - test("preserves the ACP request id across permission request and resolution events", () => { + test("translates ACP permission metadata without duplicating host lifecycle events", () => { const translation = toPermissionRequest({ params: { options: [ @@ -604,32 +971,17 @@ describe("ACP runtime event translation", () => { runId: RUN_ID, }); - const permissionEvent = translation.events.find( - (event) => event.kind === "permission.requested", - ); - - expect(permissionEvent).toBeDefined(); - expect(translation.requestId).toBe("rpc-42"); - expect(translation.defaultOptionId).toBe("allow"); - expect(eventPayload(permissionEvent as DriverEventInput)).toMatchObject({ - defaultOptionId: "allow", + expect(translation.events.map((event) => event.kind)).toEqual(["tool.call.updated"]); + expect(translation.request).toEqual({ + rawInput: '{"command":"pwd"}', requestId: "rpc-42", - targetItemId: "tool-1", title: "Run command", + toolCallId: "tool-1", + toolKind: "shell", }); - - const resolved = toPermissionResolvedEvent({ - option: translation.options[0] ?? null, - requestId: translation.requestId, - runId: RUN_ID, - }); - - expect(resolved.kind).toBe("permission.resolved"); - expect(eventPayload(resolved)).toMatchObject({ - optionId: "allow", - optionKind: "allow_once", - outcome: "selected", - requestId: "rpc-42", - }); + expect(translation.options).toEqual([ + { kind: "allow_once", name: "Allow once", optionId: "allow" }, + { kind: "reject_once", name: "Reject once", optionId: "reject" }, + ]); }); }); diff --git a/tests/acp-event-translator-session-update.test.ts b/tests/acp-event-translator-session-update.test.ts index 88095fa..354217b 100644 --- a/tests/acp-event-translator-session-update.test.ts +++ b/tests/acp-event-translator-session-update.test.ts @@ -2,7 +2,12 @@ import { describe, expect, test } from "bun:test"; import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; -import { AcpTurnEventState, toSessionReadyEvents } from "../src/runtimes/acp/acp-event-translator"; +import { AcpAssistantTranscriptState } from "../src/runtimes/acp/acp-assistant-transcript-state"; +import { + toAuthEvent, + toInitializeEvents, + toSessionReadyEvents, +} from "../src/runtimes/acp/acp-session-events"; const RUN_ID = "run-1" as RunId; @@ -37,12 +42,11 @@ function requireEvent(events: readonly DriverEventInput[], kind: string): Driver describe("ACP runtime event translation", () => { test("starts permission tool calls through the turn event state", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); const translation = state.translatePermission({ @@ -64,7 +68,6 @@ describe("ACP runtime event translation", () => { "message.started", "item.started", "tool.call.updated", - "permission.requested", "message.completed", "tool.call.updated", "item.completed", @@ -73,12 +76,11 @@ describe("ACP runtime event translation", () => { }); test("closes unfinished tool calls when a turn completes", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); const events = [ @@ -112,13 +114,12 @@ describe("ACP runtime event translation", () => { }); }); - test("maps max turn request stops to completed limited runs", () => { - const state = new AcpTurnEventState(); + test("fails open items when max turn requests stops a prompt", () => { + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); const events = [ @@ -137,31 +138,84 @@ describe("ACP runtime event translation", () => { "message.started", "item.started", "tool.call.updated", - "message.completed", + "message.failed", "tool.call.updated", "item.completed", - "run.completed", + "run.failed", ]); expect(eventPayload(events[4]!)).toMatchObject({ - status: "completed", + error: "ACP prompt stopped with max_turn_requests.", + status: "failed", toolCallId: "tool-1", }); expect(eventPayload(events[5]!)).toMatchObject({ + error: "ACP prompt stopped with max_turn_requests.", itemId: "tool-1", - status: "completed", + status: "failed", }); expect(eventPayload(events[6]!)).toMatchObject({ + error: { + code: "acp.max_turn_requests", + message: "ACP prompt stopped with max_turn_requests.", + }, + recoverable: false, stopReason: "max_turn_requests", }); }); + test("cancels every open item when the prompt is cancelled", () => { + const state = new AcpAssistantTranscriptState(); + + state.begin({ messageId: "message-1", runId: RUN_ID }); + const events = [ + ...state.translateUpdate({ + update: { + content: { text: "partial answer", type: "text" }, + messageId: "native-message-1", + sessionUpdate: "agent_message_chunk", + }, + }), + ...state.translateUpdate({ + update: { + content: { text: "partial thought", type: "text" }, + messageId: "native-message-1", + sessionUpdate: "agent_thought_chunk", + }, + }), + ...state.translateUpdate({ + update: { + sessionUpdate: "tool_call", + status: "running", + title: "Run command", + toolCallId: "tool-1", + }, + }), + ...state.completePrompt("cancelled", null), + ]; + + expect(eventKinds(events)).toContain("message.cancelled"); + expect(eventKinds(events)).toContain("thought.cancelled"); + expect(eventKinds(events)).toContain("run.cancelled"); + expect( + events.some( + (event) => + event.kind === "tool.call.updated" && eventPayload(event)["status"] === "cancelled", + ), + ).toBe(true); + expect( + events.some( + (event) => event.kind === "item.completed" && eventPayload(event)["status"] === "cancelled", + ), + ).toBe(true); + expect(eventKinds(events)).not.toContain("message.completed"); + }); + test("fails an empty end turn instead of reporting a blank completed run", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); const events = state.completePrompt("end_turn", { @@ -182,12 +236,11 @@ describe("ACP runtime event translation", () => { }); test("ignores ACP user message echo chunks because driver input is the source of truth", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); expect( @@ -204,15 +257,14 @@ describe("ACP runtime event translation", () => { }); test("promotes message-scoped thought-only ACP output into an assistant message", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); - const events = [ + const streamed = [ ...state.translateUpdate({ update: { content: { @@ -233,15 +285,16 @@ describe("ACP runtime event translation", () => { sessionUpdate: "agent_thought_chunk", }, }), - ...state.completePrompt("end_turn", null), ]; + const terminal = state.completePrompt("end_turn", null); + const events = [...streamed, ...terminal]; expect(eventKinds(events)).toEqual([ "thought.started", "thought.delta", "thought.delta", "message.started", - "message.delta", + "message.added", "message.completed", "thought.completed", "run.completed", @@ -249,23 +302,22 @@ describe("ACP runtime event translation", () => { const fallbackMessageId = eventPayloadString(events[3]!, "messageId"); expect(eventPayload(events[4]!)).toMatchObject({ - contentDelta: "\npong\n", + content: "\npong\n", messageId: fallbackMessageId, - role: "agent", }); - expect(eventPayload(events[7]!)).toMatchObject({ + expect(eventPayload(events[7]!)).toEqual({ finalMessageId: fallbackMessageId, - finalMessageText: "\npong\n", + stopReason: "end_turn", }); + expect(terminal.every((event) => event.delivery !== "best_effort")).toBe(true); }); test("treats a different native thought as the final assistant message boundary", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); const progressText = "PROGRESS:正在整理资料。"; @@ -287,38 +339,47 @@ describe("ACP runtime event translation", () => { }), ...state.completePrompt("end_turn", null), ]; - const messageDeltas = events.filter((event) => event.kind === "message.delta"); - const progressMessageId = eventPayloadString(messageDeltas[0]!, "messageId"); - const finalMessageId = eventPayloadString(messageDeltas[1]!, "messageId"); + const progressMessageId = eventPayloadString( + requireEvent(events, "message.delta"), + "messageId", + ); + const finalMessageId = eventPayloadString( + events.find( + (event) => event.kind === "message.added" && eventPayload(event)["content"] === finalText, + )!, + "messageId", + ); const completed = requireEvent(events, "run.completed"); expect(eventKinds(events)).toEqual([ "message.started", "message.delta", + "message.added", "message.completed", "thought.started", "thought.delta", "message.started", - "message.delta", + "message.added", "message.completed", "thought.completed", "run.completed", ]); expect(progressMessageId).not.toBe(finalMessageId); - expect(eventPayload(completed)).toMatchObject({ - finalMessageId, - finalMessageText: finalText, - }); - expect(eventPayloadString(completed, "finalMessageText")).not.toContain(progressText); + expect(eventPayload(completed)).toEqual({ finalMessageId, stopReason: "end_turn" }); + expect( + events.find( + (event) => + event.kind === "message.added" && eventPayload(event)["messageId"] === finalMessageId, + )?.payload, + ).toMatchObject({ content: finalText }); }); test("fails closed when an anonymous thought follows identified progress", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); const events = [ @@ -342,6 +403,7 @@ describe("ACP runtime event translation", () => { expect(eventKinds(events)).toEqual([ "message.started", "message.delta", + "message.added", "message.completed", "thought.started", "thought.delta", @@ -358,12 +420,11 @@ describe("ACP runtime event translation", () => { }); test("closes open stream items before a failed turn event", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); const events = [ @@ -395,11 +456,14 @@ describe("ACP runtime event translation", () => { "message.delta", "item.started", "tool.call.updated", - "message.completed", + "message.failed", "tool.call.updated", "item.completed", "run.failed", ]); + expect(eventPayload(requireEvent(events, "run.failed"))).toMatchObject({ + error: { message: "transport closed" }, + }); }); test("emits native resume state from ACP session setup", () => { @@ -407,7 +471,10 @@ describe("ACP runtime event translation", () => { mode: "created", nativeSessionId: "native-session-1", setup: { - currentModeId: "default", + modes: { + availableModes: [{ id: "default", name: "Default" }], + currentModeId: "default", + }, }, }); @@ -420,6 +487,11 @@ describe("ACP runtime event translation", () => { expect(events[1]).toMatchObject({ visibility: "owner_debug", }); + expect(events[2]?.delivery).toBe("best_effort"); + expect(eventPayload(events[2]!)).toEqual({ + availableModes: [{ id: "default", name: "Default" }], + currentMode: "default", + }); }); test("normalizes ACP config options to the mosoo session config contract", () => { @@ -447,6 +519,7 @@ describe("ACP runtime event translation", () => { const configEvent = events.find((event) => event.kind === "session.config.updated"); const payload = eventPayload(configEvent as DriverEventInput); + expect(configEvent?.delivery).toBe("best_effort"); expect(payload).toEqual({ options: [ { @@ -523,7 +596,7 @@ describe("ACP runtime event translation", () => { }); test("normalizes ACP commands to the mosoo session commands contract", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); const events = state.translateUpdate({ update: { availableCommands: [ @@ -551,13 +624,108 @@ describe("ACP runtime event translation", () => { ]); }); + test("fails closed on an oversized lossless session snapshot", () => { + const state = new AcpAssistantTranscriptState(); + + expect(() => + state.translateUpdate({ + update: { + availableCommands: [{ description: "x".repeat(600_000), name: "huge" }], + sessionUpdate: "available_commands_update", + }, + }), + ).toThrow("ACP session.commands.updated event exceeds 524288 UTF-8 bytes"); + }); + + test("bounds provider IDs without copying the native session ID into source IDs", () => { + const nativeSessionId = "s".repeat(600_000); + + expect(() => toSessionReadyEvents({ mode: "created", nativeSessionId, setup: {} })).toThrow( + "ACP runtime.resume.updated event exceeds 524288 UTF-8 bytes", + ); + expect(() => toAuthEvent({ methodId: nativeSessionId, status: "authenticated" })).toThrow( + "ACP auth.session.updated event exceeds 524288 UTF-8 bytes", + ); + + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); + const sourceEventId = state.translateUpdate({ + update: { + content: { text: "hello", type: "text" }, + sessionUpdate: "agent_message_chunk", + }, + })[1]?.sourceEventId; + + expect(sourceEventId).toBe("acp:run-1:agent-message:1"); + }); + + test("keeps unbounded initialize telemetry best effort", () => { + const value = "x".repeat(600_000); + const events = toInitializeEvents({ + agentCapabilities: { _meta: { value } }, + authMethods: [{ id: value, name: value }], + protocolVersion: 1, + } as never); + + expect(events.map((event) => event.delivery)).toEqual(["best_effort", "best_effort"]); + }); + + test("maps ACP 1.3 session timestamps and ignores removed setup aliases", () => { + const state = new AcpAssistantTranscriptState(); + const info = state.translateUpdate({ + update: { + sessionUpdate: "session_info_update", + title: "Session title", + updatedAt: "2026-08-13T00:00:00.000Z", + }, + }); + const legacyReady = toSessionReadyEvents({ + mode: "created", + nativeSessionId: "native-session-1", + setup: { + capabilities: { fileSystem: true }, + currentModeId: "legacy-mode", + currentModel: "legacy-model", + models: ["legacy-model"], + options: [], + providers: ["legacy-provider"], + sessionCapabilities: { legacy: true }, + visibleModes: [], + }, + }); + + expect(info).toEqual([ + { + kind: "session.info.updated", + payload: { + title: "Session title", + updatedAt: "2026-08-13T00:00:00.000Z", + }, + }, + ]); + expect( + [ + { sessionUpdate: "session_info_update", title: "" }, + { sessionUpdate: "session_info_update", updatedAt: "not-a-time" }, + ].flatMap((update) => state.translateUpdate({ update })), + ).toEqual([]); + expect(eventKinds(legacyReady)).toEqual(["session.created", "runtime.resume.updated"]); + expect( + state.translateUpdate({ + update: { + commands: [{ name: "legacy" }], + sessionUpdate: "available_commands_update", + }, + }), + ).toEqual([]); + }); + test("normalizes ACP usage sources to the mosoo usage contract", () => { - const state = new AcpTurnEventState(); + const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID, - sessionId: "session-1", }); const sessionUsage = state.translateUpdate({ @@ -623,8 +791,8 @@ describe("ACP runtime event translation", () => { ["unsafe", Number.MAX_SAFE_INTEGER + 1], ["non-finite", Number.POSITIVE_INFINITY], ] as const)("drops wholly invalid %s usage without blocking Run completion", (_name, value) => { - const state = new AcpTurnEventState(); - state.begin({ messageId: "message-1", runId: RUN_ID, sessionId: "session-1" }); + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); state.translateUpdate({ update: { content: { text: "done", type: "text" }, @@ -654,8 +822,8 @@ describe("ACP runtime event translation", () => { }); test("keeps valid usage fields while omitting malformed siblings", () => { - const state = new AcpTurnEventState(); - state.begin({ messageId: "message-1", runId: RUN_ID, sessionId: "session-1" }); + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); const [sessionUsage] = state.translateUpdate({ update: { diff --git a/tests/acp-file-system.test.ts b/tests/acp-file-system.test.ts index 3a91aa5..3867b87 100644 --- a/tests/acp-file-system.test.ts +++ b/tests/acp-file-system.test.ts @@ -32,6 +32,7 @@ function createContext(events: DriverEventInput[]): { return { context: createAgentDriverContext({ eventSink: { + currentRunId: () => null, pushEvents: async (input) => { events.push(...input.events); return { diff --git a/tests/acp-provider-fixtures.test.ts b/tests/acp-provider-fixtures.test.ts index 0c91f7c..d45b850 100644 --- a/tests/acp-provider-fixtures.test.ts +++ b/tests/acp-provider-fixtures.test.ts @@ -3,8 +3,11 @@ import type { StopReason } from "@agentclientprotocol/sdk"; import { readFileSync } from "node:fs"; import type { DriverEventInput } from "../src/protocol/events"; -import { AcpTurnEventState, toSessionReadyEvents } from "../src/runtimes/acp/acp-event-translator"; -import type { AcpTurnEventStateInput } from "../src/runtimes/acp/acp-event-translator"; +import { + AcpAssistantTranscriptState, + type AcpAssistantTranscriptStateInput, +} from "../src/runtimes/acp/acp-assistant-transcript-state"; +import { toSessionReadyEvents } from "../src/runtimes/acp/acp-session-events"; interface CompletePromptFixture { readonly stopReason: StopReason; @@ -29,7 +32,7 @@ interface SessionReadyFixture { } interface AcpProviderFixtureCase { - readonly begin?: AcpTurnEventStateInput | undefined; + readonly begin?: AcpAssistantTranscriptStateInput | undefined; readonly completePrompt?: CompletePromptFixture | undefined; readonly expectedEvents: readonly unknown[]; readonly failPrompt?: FailPromptFixture | undefined; @@ -74,7 +77,7 @@ function readRecordField(record: Record, field: string): Record return value; } -function readBeginFixture(value: unknown): AcpTurnEventStateInput | undefined { +function readBeginFixture(value: unknown): AcpAssistantTranscriptStateInput | undefined { if (value === undefined) { return undefined; } @@ -85,8 +88,7 @@ function readBeginFixture(value: unknown): AcpTurnEventStateInput | undefined { return { messageId: readStringField(value, "messageId"), - runId: readStringField(value, "runId") as AcpTurnEventStateInput["runId"], - sessionId: readStringField(value, "sessionId"), + runId: readStringField(value, "runId") as AcpAssistantTranscriptStateInput["runId"], }; } @@ -277,7 +279,7 @@ function normalizeAcpEvents(events: readonly DriverEventInput[]): Record { - test("drops additional directories when the agent does not advertise support", async () => { + test("rejects additional directories when the agent does not advertise support", async () => { const { connection, requests } = createRecordingConnection(); - const setup = await setupAcpSession( - setupInput({ - agentCapabilities: { - loadSession: true, - sessionCapabilities: { close: {}, resume: {} }, - }, - connection, - currentSessionId: null, - payload: withAdditionalDirectories(["/workspace/extra"]), - replaySession: async (operation) => operation(), - }), - ); - - expect(setup.mode).toBe("created"); - expect(setup.sessionId).toBe("acp-session-1"); - expect(setup.droppedAdditionalDirectories).toEqual(["/workspace/extra"]); - expect(requests).toHaveLength(1); - expect(requests[0]?.method).toBe(acpMethods.agent.session.new); - expect("additionalDirectories" in (requests[0]?.params ?? {})).toBe(false); + await expect( + setupAcpSession( + setupInput({ + agentCapabilities: { + loadSession: true, + sessionCapabilities: { close: {}, resume: {} }, + }, + connection, + currentSessionId: null, + payload: withAdditionalDirectories(["/workspace/extra"]), + replaySession: async (operation) => operation(), + }), + ), + ).rejects.toThrow("does not advertise additionalDirectories support"); + expect(requests).toHaveLength(0); }); test("passes additional directories through when the agent advertises support", async () => { const { connection, requests } = createRecordingConnection(); - const setup = await setupAcpSession( + await setupAcpSession( setupInput({ agentCapabilities: { loadSession: true, @@ -94,7 +90,6 @@ describe("ACP session setup", () => { }), ); - expect(setup.droppedAdditionalDirectories).toEqual([]); expect(requests[0]?.params["additionalDirectories"]).toEqual(["/workspace/extra"]); }); @@ -210,24 +205,21 @@ describe("ACP session setup", () => { }); }); - test("creates a new session when native restoration is unavailable", async () => { + test("rejects a native session when restoration is unavailable", async () => { const methods: string[] = []; - const result = await setupAcpSession( - setupInput({ - agentCapabilities: {}, - connection: connectionWith(async (method) => { - methods.push(method); - return { sessionId: "native-session-new" }; + await expect( + setupAcpSession( + setupInput({ + agentCapabilities: {}, + connection: connectionWith(async (method) => { + methods.push(method); + return { sessionId: "native-session-new" }; + }), + currentSessionId: EXISTING_SESSION_ID, + replaySession: async (operation) => operation(), }), - currentSessionId: EXISTING_SESSION_ID, - replaySession: async (operation) => operation(), - }), - ); - - expect(result).toMatchObject({ - mode: "created", - sessionId: "native-session-new", - }); - expect(methods).toEqual([acpMethods.agent.session.new]); + ), + ).rejects.toThrow("cannot restore the requested native session"); + expect(methods).toEqual([]); }); }); diff --git a/tests/acp-terminal-manager.test.ts b/tests/acp-terminal-manager.test.ts index 8d4d795..45c24f7 100644 --- a/tests/acp-terminal-manager.test.ts +++ b/tests/acp-terminal-manager.test.ts @@ -27,7 +27,10 @@ function createHarness( sink: async () => {}, }); const context = createAgentDriverContext({ - eventSink: { pushEvents: async () => ({ accepted: [] }) }, + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, logger, payload: driverStartInput, permission: { request: async () => "reject_once" }, @@ -429,38 +432,32 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } }); - test("reuses one supervisor across the full terminal capacity", async () => { + test("reuses one supervisor for concurrent terminals", async () => { const supervisors = new Set(); - let leaseCount = 0; - const harness = createHarness(undefined, 32, false, (pid, marker) => { + const harness = createHarness(undefined, 2, false, (pid, marker) => { const lease = spawnLinuxProcessTreeWatchdog(pid, marker); if (lease === null) { throw new Error("Test process supervision could not start."); } - leaseCount += 1; supervisors.add(lease.process); return lease; }); try { await Promise.all( - Array.from({ length: 32 }, () => + Array.from({ length: 2 }, () => harness.manager.create(harness.context, { args: ["-e", "setInterval(() => {}, 1_000)"], command: process.execPath, }), ), ); - - expect(leaseCount).toBe(32); expect(supervisors.size).toBe(1); - expect([...supervisors][0]?.pid).toBeGreaterThan(1); - await harness.manager.stopAll(harness.context); } finally { await harness.manager.stopAll(harness.context).catch(() => {}); await harness.logger.destroy(); } - }, 10_000); + }); test("rejects terminal buffers above the deployment hard limit", async () => { const harness = createHarness(); @@ -502,6 +499,69 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } }); + test("reserves capacity before concurrent terminal creation can yield", async () => { + const harness = createHarness(undefined, 1); + + try { + const creations = await Promise.allSettled( + Array.from({ length: 8 }, () => + harness.manager.create(harness.context, { + args: ["-e", "setInterval(() => {}, 1000)"], + command: process.execPath, + }), + ), + ); + const created = creations.flatMap((result) => + result.status === "fulfilled" ? [result.value] : [], + ); + + expect(created).toHaveLength(1); + expect(creations.filter((result) => result.status === "rejected")).toHaveLength(7); + expect(harness.events.filter((event) => event.kind === "terminal.created")).toHaveLength(1); + + await harness.manager.release(harness.context, created[0]); + const next = await harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }); + await harness.manager.waitForExit(next); + await harness.manager.release(harness.context, next); + } finally { + await harness.manager.stopAll(harness.context); + await harness.logger.destroy(); + } + }); + + test("stops only one turn and accepts terminals in the next turn", async () => { + const harness = createHarness(undefined, 1); + + try { + const firstTurn = harness.manager.beginTurn(); + const first = await harness.manager.create(harness.context, { + args: ["-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)'], + command: process.execPath, + }); + while (harness.manager.output(first).output !== "ready") { + await Bun.sleep(10); + } + + await harness.manager.stopTurn(harness.context, firstTurn); + expect(() => harness.manager.output(first)).toThrow("does not exist"); + + const secondTurn = harness.manager.beginTurn(); + const second = await harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }); + await harness.manager.waitForExit(second); + await harness.manager.stopTurn(harness.context, secondTurn); + expect(() => harness.manager.output(second)).toThrow("does not exist"); + } finally { + await harness.manager.stopAll(harness.context); + await harness.logger.destroy(); + } + }); + test.each([0, Number.POSITIVE_INFINITY, 1.5])("rejects invalid terminal limit %p", (value) => { expect(() => createHarness(undefined, value)).toThrow("positive safe integer"); }); diff --git a/tests/acp-v1-contract-adapter-mapping.test.ts b/tests/acp-v1-contract-adapter-mapping.test.ts deleted file mode 100644 index ac91a69..0000000 --- a/tests/acp-v1-contract-adapter-mapping.test.ts +++ /dev/null @@ -1,506 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { RequestPermissionRequest, SessionNotification } from "@agentclientprotocol/sdk"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - Run, - SessionSnapshot, -} from "../src/contract"; -import { AcpV1ContractAdapter } from "../src/runtimes/acp/v1-contract-adapter"; -import { - type ContractAuthorityUpdate, - type ContractPreviewUpdate, -} from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const NATIVE_SESSION_ID = "native-session-1"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - "interaction.permission": {}, - "item.artifact": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - interactionTimeoutMs = 5 * 60 * 1_000, - previewCheckpointBytes?: number, - maxPendingPermissionBytes?: number, - beforeAuthority?: (update: ContractAuthorityUpdate) => Promise, - afterAuthority?: (update: ContractAuthorityUpdate) => Promise, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - const authority: ContractAuthorityUpdate[] = []; - const committedMutationIds = new Set(); - const previews: ContractPreviewUpdate[] = []; - const commit = (cause: CommittedMutation["cause"], operations: AuthorityOperation[]): void => { - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId: protocolId(1_000 + revision), - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - }; - const adapter = new AcpV1ContractAdapter({ - authority: async (update) => { - await beforeAuthority?.(update); - authority.push(update); - if (!committedMutationIds.has(update.mutationId)) { - committedMutationIds.add(update.mutationId); - commit(update.cause, [...update.operations] as AuthorityOperation[]); - } - try { - await afterAuthority?.(update); - } catch (cause) { - throw new AuthorityOutcomeUnknownError( - cause instanceof Error ? cause.message : "Authority outcome is unknown.", - { cause }, - ); - } - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs, - maxPendingPermissionBytes, - nativeSessionId: NATIVE_SESSION_ID, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - previewCheckpointBytes, - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - previews, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: resolution.value, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -async function registerRun(adapter: AcpV1ContractAdapter): Promise { - adapter.attachRun(activeRun("2026-07-16T08:00:00.000Z")); -} - -function notification(update: SessionNotification["update"]): SessionNotification { - return { sessionId: NATIVE_SESSION_ID, update }; -} - -describe("ACP V1 Contract adapter", () => { - test("preserves streamed rich-content order and repairs Preview at prompt completion", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: { text: "hello ", type: "text" }, - messageId: "message-1", - sessionUpdate: "agent_message_chunk", - }), - ); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: { data: "aA==", mimeType: "image/png", type: "image" }, - messageId: "message-1", - sessionUpdate: "agent_message_chunk", - }), - ); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: { text: "world", type: "text" }, - messageId: "message-1", - sessionUpdate: "agent_message_chunk", - }), - ); - await harness.adapter.completePrompt(RUN_ID, { - stopReason: "end_turn", - usage: { - cachedReadTokens: 2, - inputTokens: 3, - outputTokens: 5, - thoughtTokens: 1, - totalTokens: 8, - }, - }); - - expect(harness.previews.map((entry) => entry.update)).toMatchObject([ - { op: "append", segment: 0, text: "hello " }, - { op: "append", segment: 2, text: "world" }, - ]); - expect(harness.snapshot().items).toContainEqual( - expect.objectContaining({ - content: [ - { text: "hello ", type: "text" }, - { data: "aA==", mediaType: "image/png", type: "inline_blob" }, - { text: "world", type: "text" }, - ], - id: "message:message-1", - kind: "message", - status: "completed", - }), - ); - expect(harness.snapshot().runs[0]).toMatchObject({ - finishReason: "success", - status: "completed", - usage: { - cachedInput: 2, - input: 3, - output: 5, - reasoning: 1, - total: 8, - }, - }); - }); - - test.each([ - ["negative", -1], - ["fractional", 1.5], - ["unsafe", Number.MAX_SAFE_INTEGER + 1], - ["non-finite", Number.POSITIVE_INFINITY], - ] as const)("ignores %s prompt usage without blocking the Run terminal", async (_name, value) => { - const harness = createHarness(); - await registerRun(harness.adapter); - - await harness.adapter.completePrompt(RUN_ID, { - stopReason: "end_turn", - usage: { - cachedReadTokens: value, - inputTokens: value, - outputTokens: value, - thoughtTokens: value, - totalTokens: value, - }, - }); - - expect(harness.snapshot().runs[0]).toMatchObject({ - finishReason: "success", - status: "completed", - }); - expect(harness.snapshot().runs[0]).not.toHaveProperty("usage"); - }); - - test("retains valid prompt usage while omitting malformed fields", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - - await harness.adapter.completePrompt(RUN_ID, { - stopReason: "end_turn", - usage: { - cachedReadTokens: 1.5, - inputTokens: -1, - outputTokens: 2, - thoughtTokens: Number.MAX_SAFE_INTEGER + 1, - totalTokens: 2, - }, - }); - - expect(harness.snapshot().runs[0]).toMatchObject({ - finishReason: "success", - status: "completed", - usage: { output: 2, total: 2 }, - }); - }); - - test("projects tool diffs and terminal snapshots, then treats request limits as limits", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: [ - { - newText: "new\n", - oldText: "old\n", - path: "/workspace/file.txt", - type: "diff", - }, - { terminalId: "terminal-1", type: "terminal" }, - ], - kind: "execute", - rawInput: { command: "printf new" }, - sessionUpdate: "tool_call", - status: "in_progress", - title: "Update file", - toolCallId: "tool-1", - }), - ); - await harness.adapter.handleTerminalOutput(RUN_ID, "terminal-1", { - exitStatus: { exitCode: 0, signal: null }, - output: "new\n", - truncated: false, - }); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - rawOutput: { changed: true }, - sessionUpdate: "tool_call_update", - status: "completed", - toolCallId: "tool-1", - }), - ); - await harness.adapter.completePrompt(RUN_ID, { stopReason: "max_turn_requests" }); - - expect(harness.snapshot().items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "tool:tool-1", - kind: "tool", - status: "completed", - structuredOutput: { changed: true }, - terminalItemId: "terminal:terminal-1", - }), - expect.objectContaining({ - changes: [ - expect.objectContaining({ - operation: "update", - path: "/workspace/file.txt", - }), - ], - id: "change:tool-1", - kind: "change", - status: "completed", - }), - expect.objectContaining({ - exitCode: 0, - id: "terminal:terminal-1", - kind: "terminal", - status: "completed", - stdout: [{ text: "new\n", type: "text" }], - }), - ]), - ); - expect(harness.snapshot().runs[0]).toMatchObject({ - finishReason: "limit", - status: "completed", - }); - }); - - test("translates accepted permission decisions and leaves deadline ownership to the Coordinator", async () => { - const harness = createHarness(1_000); - await registerRun(harness.adapter); - const request = { - options: [ - { kind: "allow_once", name: "Allow", optionId: "yes" }, - { kind: "reject_once", name: "Deny", optionId: "no" }, - ], - sessionId: NATIVE_SESSION_ID, - toolCall: { - title: "Run command", - toolCallId: "tool-1", - }, - } satisfies RequestPermissionRequest; - const interactionId = await harness.adapter.openPermission(RUN_ID, request); - expect(await harness.adapter.openPermission(RUN_ID, request)).toBe(interactionId); - expect(harness.snapshot().interactions).toHaveLength(1); - const interaction = harness.snapshot().interactions.find((entry) => entry.id === interactionId); - const allowId = - interaction?.kind === "permission" - ? interaction.request.options.find((option) => option.effect === "allow")?.id - : undefined; - const resolution = { - kind: "permission", - value: { optionId: allowId!, type: "selected" }, - } satisfies InteractionResolution; - - harness.settleInteraction(interactionId, resolution); - await expect(harness.adapter.resolveInteraction(interactionId, resolution)).resolves.toEqual({ - outcome: { optionId: "yes", outcome: "selected" }, - }); - expect( - harness.snapshot().interactions.find((entry) => entry.id === interactionId), - ).toMatchObject({ status: "resolved" }); - - const lateId = await harness.adapter.openPermission(RUN_ID, { - ...request, - toolCall: { title: "Read file", toolCallId: "tool-2" }, - }); - harness.advance(1_001); - harness.settleInteraction(lateId); - await expect( - harness.adapter.resolveInteraction(lateId, { - kind: "permission", - value: { type: "cancelled" }, - }), - ).resolves.toEqual({ outcome: { outcome: "cancelled" } }); - expect(harness.snapshot().interactions.find((entry) => entry.id === lateId)).toMatchObject({ - status: "expired", - }); - - await harness.adapter.completePrompt(RUN_ID, { stopReason: "cancelled" }); - expect(harness.snapshot().runs[0]?.status).toBe("cancelled"); - expect(harness.snapshot().interactions.every((entry) => entry.status !== "open")).toBe(true); - }); - - test.each([ - [ - "tool payload", - (request: RequestPermissionRequest): RequestPermissionRequest => ({ - ...request, - toolCall: { ...request.toolCall, title: "Changed command" }, - }), - ], - [ - "permission options", - (request: RequestPermissionRequest): RequestPermissionRequest => ({ - ...request, - options: [{ ...request.options[0]!, name: "Changed choice" }], - }), - ], - ] as const)("rejects a permission replay with changed %s", async (_name, change) => { - const harness = createHarness(); - await registerRun(harness.adapter); - const request = { - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId: "tool-replay" }, - } satisfies RequestPermissionRequest; - await harness.adapter.openPermission(RUN_ID, request); - const before = harness.authority.length; - - await expect(harness.adapter.openPermission(RUN_ID, change(request))).rejects.toThrow( - "changed identity", - ); - expect(harness.authority).toHaveLength(before); - expect(harness.snapshot().interactions).toHaveLength(1); - }); - - test("bounds pending permission payloads and restores budget after resolution", async () => { - const request = (toolCallId: string) => - ({ - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId }, - }) satisfies RequestPermissionRequest; - const firstRequest = request("tool-1"); - const secondRequest = request("tool-2"); - const maxPendingPermissionBytes = new TextEncoder().encode( - JSON.stringify(firstRequest), - ).byteLength; - const harness = createHarness(5 * 60 * 1_000, undefined, maxPendingPermissionBytes); - await registerRun(harness.adapter); - const interactionId = await harness.adapter.openPermission(RUN_ID, firstRequest); - const authorityCount = harness.authority.length; - - await expect(harness.adapter.openPermission(RUN_ID, secondRequest)).rejects.toThrow( - "pending permission budget", - ); - expect(harness.authority).toHaveLength(authorityCount); - - const resolution = { - kind: "permission", - value: { type: "cancelled" }, - } satisfies InteractionResolution; - harness.settleInteraction(interactionId, resolution); - await harness.adapter.resolveInteraction(interactionId, resolution); - await expect(harness.adapter.openPermission(RUN_ID, secondRequest)).resolves.toBeDefined(); - }); - - test("coalesces concurrent permission replays within one byte reservation", async () => { - const blocked = Promise.withResolvers(); - const release = Promise.withResolvers(); - const request = { - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId: "tool-concurrent" }, - } satisfies RequestPermissionRequest; - const limit = new TextEncoder().encode(JSON.stringify(request)).byteLength; - let first = true; - const harness = createHarness(5 * 60 * 1_000, undefined, limit, async () => { - if (first) { - first = false; - blocked.resolve(); - await release.promise; - } - }); - await registerRun(harness.adapter); - const firstPermission = harness.adapter.openPermission(RUN_ID, request); - await blocked.promise; - const secondPermission = harness.adapter.openPermission(RUN_ID, request); - - release.resolve(); - const [firstId, secondId] = await Promise.all([firstPermission, secondPermission]); - expect(secondId).toBe(firstId); - expect(harness.snapshot().interactions).toHaveLength(1); - }); -}); diff --git a/tests/acp-v1-contract-adapter-permission.test.ts b/tests/acp-v1-contract-adapter-permission.test.ts deleted file mode 100644 index 24723d3..0000000 --- a/tests/acp-v1-contract-adapter-permission.test.ts +++ /dev/null @@ -1,464 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { RequestPermissionRequest, SessionNotification } from "@agentclientprotocol/sdk"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - Run, - SessionSnapshot, -} from "../src/contract"; -import { AcpV1ContractAdapter } from "../src/runtimes/acp/v1-contract-adapter"; -import { - type ContractAuthorityUpdate, - type ContractPreviewUpdate, -} from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const NATIVE_SESSION_ID = "native-session-1"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - "interaction.permission": {}, - "item.artifact": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - interactionTimeoutMs = 5 * 60 * 1_000, - previewCheckpointBytes?: number, - maxPendingPermissionBytes?: number, - beforeAuthority?: (update: ContractAuthorityUpdate) => Promise, - afterAuthority?: (update: ContractAuthorityUpdate) => Promise, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - const authority: ContractAuthorityUpdate[] = []; - const committedMutationIds = new Set(); - const previews: ContractPreviewUpdate[] = []; - const commit = (cause: CommittedMutation["cause"], operations: AuthorityOperation[]): void => { - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId: protocolId(1_000 + revision), - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - }; - const adapter = new AcpV1ContractAdapter({ - authority: async (update) => { - await beforeAuthority?.(update); - authority.push(update); - if (!committedMutationIds.has(update.mutationId)) { - committedMutationIds.add(update.mutationId); - commit(update.cause, [...update.operations] as AuthorityOperation[]); - } - try { - await afterAuthority?.(update); - } catch (cause) { - throw new AuthorityOutcomeUnknownError( - cause instanceof Error ? cause.message : "Authority outcome is unknown.", - { cause }, - ); - } - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs, - maxPendingPermissionBytes, - nativeSessionId: NATIVE_SESSION_ID, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - previewCheckpointBytes, - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - previews, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: resolution.value, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -async function registerRun(adapter: AcpV1ContractAdapter): Promise { - adapter.attachRun(activeRun("2026-07-16T08:00:00.000Z")); -} - -function notification(update: SessionNotification["update"]): SessionNotification { - return { sessionId: NATIVE_SESSION_ID, update }; -} - -describe("ACP V1 Contract adapter", () => { - test("retries the exact permission intent after an ambiguous authority result", async () => { - let failAfterCommit = true; - const harness = createHarness( - 5 * 60 * 1_000, - undefined, - undefined, - undefined, - async (update) => { - if (failAfterCommit && update.event === "permission/requested") { - failAfterCommit = false; - throw new Error("ambiguous authority result"); - } - }, - ); - await registerRun(harness.adapter); - const request = { - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId: "tool-ambiguous" }, - } satisfies RequestPermissionRequest; - - await expect(harness.adapter.openPermission(RUN_ID, request)).rejects.toThrow("ambiguous"); - const first = harness.authority.filter((update) => update.event === "permission/requested")[0]!; - harness.advance(1_000); - await expect( - harness.adapter.openPermission(RUN_ID, { - ...request, - toolCall: { ...request.toolCall, toolCallId: "tool-other" }, - }), - ).rejects.toBeInstanceOf(AuthorityOutcomeUnknownError); - await expect(harness.adapter.openPermission(RUN_ID, request)).resolves.toBe( - (first.operations[0] as { value: { id: string } }).value.id, - ); - const retries = harness.authority.filter((update) => update.event === "permission/requested"); - - expect(retries).toHaveLength(2); - expect(retries[1]).toEqual(first); - }); - - test.each(["cancelled", "selected"] as const)( - "coordinates an unknown permission replay after the Interaction is %s", - async (outcome) => { - let loseResult = true; - const request = { - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId: "tool-resolved-unknown" }, - } satisfies RequestPermissionRequest; - const limit = new TextEncoder().encode(JSON.stringify(request)).byteLength; - const harness = createHarness(5 * 60 * 1_000, undefined, limit, undefined, async (update) => { - if (loseResult && update.event === "permission/requested") { - loseResult = false; - throw new Error("authority result lost"); - } - }); - await registerRun(harness.adapter); - - await expect(harness.adapter.openPermission(RUN_ID, request)).rejects.toBeInstanceOf( - AuthorityOutcomeUnknownError, - ); - const first = harness.authority.find((update) => update.event === "permission/requested")!; - const interactionId = (first.operations[0] as { value: { id: string } }).value.id; - const resolution = - outcome === "selected" - ? ({ - kind: "permission", - value: { optionId: "permission-option:yes", type: "selected" }, - } satisfies InteractionResolution) - : ({ - kind: "permission", - value: { type: "cancelled" }, - } satisfies InteractionResolution); - harness.settleInteraction(interactionId, resolution); - await expect(harness.adapter.resolveInteraction(interactionId, resolution)).resolves.toEqual( - outcome === "selected" - ? { outcome: { optionId: "yes", outcome: "selected" } } - : { outcome: { outcome: "cancelled" } }, - ); - await expect( - harness.adapter.openPermission(RUN_ID, { - ...request, - toolCall: { ...request.toolCall, toolCallId: "tool-changed" }, - }), - ).rejects.toBeInstanceOf(AuthorityOutcomeUnknownError); - - harness.advance(1_000); - await expect(harness.adapter.openPermission(RUN_ID, request)).resolves.toBe(interactionId); - const retries = harness.authority.filter((update) => update.event === "permission/requested"); - expect(retries).toEqual([first, first]); - expect(harness.snapshot().interactions.find(({ id }) => id === interactionId)?.status).toBe( - "resolved", - ); - await expect( - harness.adapter.openPermission(RUN_ID, { - ...request, - toolCall: { ...request.toolCall, toolCallId: "tool-next-permission" }, - }), - ).resolves.toBeDefined(); - }, - ); - - test("releases a retained permission budget when its unknown retry is definitely rejected", async () => { - const request = (toolCallId: string) => - ({ - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId }, - }) satisfies RequestPermissionRequest; - const first = request("tool-a"); - const second = request("tool-b"); - const limit = new TextEncoder().encode(JSON.stringify(first)).byteLength; - let rejectRetry = false; - let loseFirstResult = true; - const harness = createHarness( - 5 * 60 * 1_000, - undefined, - limit, - async (update) => { - if (rejectRetry && update.event === "permission/requested") { - rejectRetry = false; - throw new Error("authority unavailable"); - } - }, - async (update) => { - if (loseFirstResult && update.event === "permission/requested") { - loseFirstResult = false; - rejectRetry = true; - throw new Error("authority result lost"); - } - }, - ); - await registerRun(harness.adapter); - - await expect(harness.adapter.openPermission(RUN_ID, first)).rejects.toBeInstanceOf( - AuthorityOutcomeUnknownError, - ); - await expect(harness.adapter.openPermission(RUN_ID, first)).rejects.toThrow( - "authority unavailable", - ); - await expect(harness.adapter.openPermission(RUN_ID, second)).resolves.toBeDefined(); - }); - - test("applies the permission byte budget atomically", async () => { - const request = (toolCallId: string) => - ({ - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId }, - }) satisfies RequestPermissionRequest; - const firstRequest = request("tool-aaa"); - const secondRequest = request("tool-bbb"); - const limit = new TextEncoder().encode(JSON.stringify(firstRequest)).byteLength; - const blocked = Promise.withResolvers(); - const release = Promise.withResolvers(); - let first = true; - const harness = createHarness(5 * 60 * 1_000, undefined, limit, async () => { - if (first) { - first = false; - blocked.resolve(); - await release.promise; - } - }); - await registerRun(harness.adapter); - const firstPermission = harness.adapter.openPermission(RUN_ID, firstRequest); - await blocked.promise; - const secondPermission = harness.adapter.openPermission(RUN_ID, secondRequest); - let secondState: "pending" | "rejected" | "resolved" = "pending"; - void secondPermission.then( - () => { - secondState = "resolved"; - }, - () => { - secondState = "rejected"; - }, - ); - await Promise.resolve(); - await Promise.resolve(); - - expect(secondState).toBe("rejected"); - - release.resolve(); - await expect(firstPermission).resolves.toBeDefined(); - await expect(secondPermission).rejects.toThrow("pending permission budget"); - expect(harness.snapshot().interactions).toHaveLength(1); - }); - - test.each([ - ["tool", "permission/requested.tool"], - ["interaction", "permission/requested"], - ] as const)( - "releases permission budget after a definite %s Authority rejection", - async (_stage, failedEvent) => { - const request = (toolCallId: string) => - ({ - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId }, - }) satisfies RequestPermissionRequest; - const firstRequest = request("tool-aaa"); - const secondRequest = request("tool-bbb"); - const limit = new TextEncoder().encode(JSON.stringify(firstRequest)).byteLength; - let fail = true; - const harness = createHarness(5 * 60 * 1_000, undefined, limit, async (update) => { - if (fail && update.event === failedEvent) { - fail = false; - throw new Error("authority unavailable"); - } - }); - await registerRun(harness.adapter); - - await expect(harness.adapter.openPermission(RUN_ID, firstRequest)).rejects.toThrow( - "authority unavailable", - ); - const interactionId = await harness.adapter.openPermission(RUN_ID, secondRequest); - const resolution = { - kind: "permission", - value: { type: "cancelled" }, - } satisfies InteractionResolution; - harness.settleInteraction(interactionId, resolution); - await harness.adapter.resolveInteraction(interactionId, resolution); - await expect(harness.adapter.openPermission(RUN_ID, firstRequest)).resolves.toBeDefined(); - }, - ); - - test("fails queued updates after the first projection failure", async () => { - let attempts = 0; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event === "session/agent_message_chunk") { - attempts += 1; - - if (attempts === 1) { - throw new Error("authority unavailable"); - } - } - }); - await registerRun(harness.adapter); - const update = (messageId: string) => - harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: { text: messageId, type: "text" }, - messageId, - sessionUpdate: "agent_message_chunk", - }), - ); - const settled = await Promise.allSettled([update("first"), update("second")]); - - expect(settled.map((result) => result.status)).toEqual(["rejected", "rejected"]); - expect(attempts).toBe(1); - await expect( - harness.adapter.completePrompt(RUN_ID, { stopReason: "end_turn" }), - ).rejects.toThrow("authority unavailable"); - }); - - test("recovers an ambiguous session update after rejecting a changed retry", async () => { - const mutationIds: string[] = []; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event === "session/tool_call") { - mutationIds.push(update.mutationId); - - if (mutationIds.length === 1) { - throw new AuthorityOutcomeUnknownError("authority result lost"); - } - } - }); - await registerRun(harness.adapter); - const tool = { - content: [], - kind: "execute", - rawInput: { command: "true" }, - sessionUpdate: "tool_call", - status: "in_progress", - title: "Run command", - toolCallId: "tool-1", - } satisfies SessionNotification["update"]; - const update = notification(tool); - await expect(harness.adapter.handleSessionUpdate(RUN_ID, update)).rejects.toThrow( - "authority result lost", - ); - harness.advance(1_000); - - await expect( - harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ ...tool, title: "Changed command" }), - ), - ).rejects.toThrow("changed while its outcome was unknown"); - expect(mutationIds).toHaveLength(1); - - await harness.adapter.handleSessionUpdate(RUN_ID, update); - await harness.adapter.completePrompt(RUN_ID, { stopReason: "end_turn" }); - - expect(mutationIds).toEqual([mutationIds[0], mutationIds[0]]); - expect(harness.snapshot().runs[0]?.status).toBe("completed"); - }); -}); diff --git a/tests/acp-v1-contract-adapter-run-lifecycle.test.ts b/tests/acp-v1-contract-adapter-run-lifecycle.test.ts deleted file mode 100644 index c4433ea..0000000 --- a/tests/acp-v1-contract-adapter-run-lifecycle.test.ts +++ /dev/null @@ -1,514 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { SessionNotification } from "@agentclientprotocol/sdk"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - Run, - SessionSnapshot, -} from "../src/contract"; -import { AcpV1ContractAdapter } from "../src/runtimes/acp/v1-contract-adapter"; -import { - type ContractAuthorityUpdate, - type ContractPreviewUpdate, -} from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const NATIVE_SESSION_ID = "native-session-1"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - "interaction.permission": {}, - "item.artifact": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - interactionTimeoutMs = 5 * 60 * 1_000, - previewCheckpointBytes?: number, - maxPendingPermissionBytes?: number, - beforeAuthority?: (update: ContractAuthorityUpdate) => Promise, - afterAuthority?: (update: ContractAuthorityUpdate) => Promise, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - const authority: ContractAuthorityUpdate[] = []; - const committedMutationIds = new Set(); - const previews: ContractPreviewUpdate[] = []; - const commit = (cause: CommittedMutation["cause"], operations: AuthorityOperation[]): void => { - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId: protocolId(1_000 + revision), - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - }; - const adapter = new AcpV1ContractAdapter({ - authority: async (update) => { - await beforeAuthority?.(update); - authority.push(update); - if (!committedMutationIds.has(update.mutationId)) { - committedMutationIds.add(update.mutationId); - commit(update.cause, [...update.operations] as AuthorityOperation[]); - } - try { - await afterAuthority?.(update); - } catch (cause) { - throw new AuthorityOutcomeUnknownError( - cause instanceof Error ? cause.message : "Authority outcome is unknown.", - { cause }, - ); - } - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs, - maxPendingPermissionBytes, - nativeSessionId: NATIVE_SESSION_ID, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - previewCheckpointBytes, - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - previews, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: resolution.value, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -async function registerRun(adapter: AcpV1ContractAdapter): Promise { - adapter.attachRun(activeRun("2026-07-16T08:00:00.000Z")); -} - -function notification(update: SessionNotification["update"]): SessionNotification { - return { sessionId: NATIVE_SESSION_ID, update }; -} - -describe("ACP V1 Contract adapter", () => { - test("checkpoints full terminal snapshots without retaining or duplicating large Preview text", async () => { - const harness = createHarness(5 * 60 * 1_000, 5); - await registerRun(harness.adapter); - await harness.adapter.registerTerminal(RUN_ID, "terminal-1"); - const beforeCheckpoint = harness.authority.length; - - await harness.adapter.handleTerminalOutput(RUN_ID, "terminal-1", { - exitStatus: null, - output: "abcdef", - truncated: true, - }); - expect(harness.authority).toHaveLength(beforeCheckpoint + 1); - expect(harness.previews).toHaveLength(0); - expect(harness.snapshot().items[0]).toMatchObject({ - kind: "terminal", - status: "active", - stdout: [{ text: "abcdef", type: "text" }], - }); - - await harness.adapter.handleTerminalOutput(RUN_ID, "terminal-1", { - exitStatus: null, - output: "abcdef", - truncated: false, - }); - expect(harness.authority).toHaveLength(beforeCheckpoint + 1); - - await harness.adapter.handleTerminalExit(RUN_ID, "terminal-1", { - exitCode: 0, - signal: null, - }); - expect(harness.snapshot().items[0]).toMatchObject({ kind: "terminal", status: "active" }); - - await harness.adapter.completePrompt(RUN_ID, { stopReason: "end_turn" }); - expect(harness.snapshot().items[0]).toMatchObject({ - extensions: { - "agentclientprotocol.v1/terminal-output": { truncated: true }, - }, - exitCode: 0, - kind: "terminal", - status: "completed", - stdout: [{ text: "abcdef", type: "text" }], - }); - }); - - test("accepts the final terminal snapshot after wait-for-exit resolves", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.registerTerminal(RUN_ID, "terminal-tail"); - await harness.adapter.handleTerminalOutput(RUN_ID, "terminal-tail", { - exitStatus: null, - output: "prefix", - truncated: false, - }); - await harness.adapter.handleTerminalExit(RUN_ID, "terminal-tail", { - exitCode: 0, - signal: null, - }); - await harness.adapter.handleTerminalOutput(RUN_ID, "terminal-tail", { - exitStatus: { exitCode: 0, signal: null }, - output: "prefix-tail", - truncated: false, - }); - - expect(harness.snapshot().items[0]).toMatchObject({ - exitCode: 0, - kind: "terminal", - status: "completed", - stdout: [{ text: "prefix-tail", type: "text" }], - }); - }); - - test("ignores draft v1 plan operations that were not negotiated", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - plan: { - entries: [], - planId: "draft-plan", - type: "items", - }, - sessionUpdate: "plan_update", - }), - ); - - expect(harness.snapshot().items).toHaveLength(0); - }); - - test("honors explicit empty collection replacements in tool updates", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: [ - { content: { text: "temporary", type: "text" }, type: "content" }, - { - newText: "new", - oldText: "old", - path: "/workspace/file.txt", - type: "diff", - }, - { terminalId: "terminal-1", type: "terminal" }, - ], - sessionUpdate: "tool_call", - title: "Read", - toolCallId: "tool-1", - }), - ); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: [], - sessionUpdate: "tool_call_update", - toolCallId: "tool-1", - }), - ); - - const tool = harness.snapshot().items.find((item) => item.kind === "tool"); - expect(tool).toMatchObject({ - kind: "tool", - output: [], - }); - expect(tool).not.toHaveProperty("terminalItemId"); - expect(harness.snapshot().items.find((item) => item.kind === "change")).toMatchObject({ - changes: [], - status: "active", - }); - }); - - test("treats nullable v1 tool patch fields as omitted", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: [ - { content: { text: "kept", type: "text" }, type: "content" }, - { - newText: "new", - oldText: "old", - path: "/workspace/file.txt", - type: "diff", - }, - { terminalId: "terminal-1", type: "terminal" }, - ], - kind: "edit", - locations: [{ line: 2, path: "/workspace/file.txt" }], - rawInput: { path: "/workspace/file.txt" }, - rawOutput: { changed: true }, - sessionUpdate: "tool_call", - title: "Edit", - toolCallId: "tool-1", - }), - ); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: null, - kind: null, - locations: null, - rawInput: null, - rawOutput: null, - sessionUpdate: "tool_call_update", - status: null, - title: null, - toolCallId: "tool-1", - }), - ); - - expect(harness.snapshot().items.find((item) => item.kind === "tool")).toMatchObject({ - category: "edit", - input: { path: "/workspace/file.txt" }, - locations: [{ line: 2, path: "/workspace/file.txt" }], - output: [{ text: "kept", type: "text" }], - status: "active", - structuredOutput: { changed: true }, - terminalItemId: "terminal:terminal-1", - title: "Edit", - }); - expect(harness.snapshot().items.find((item) => item.kind === "change")).toMatchObject({ - changes: [expect.objectContaining({ path: "/workspace/file.txt" })], - status: "active", - }); - }); - - test.each([ - ["raw output", { rawOutput: { late: true } }, { structuredOutput: { late: true } }], - [ - "locations", - { locations: [{ line: 7, path: "/workspace/late.txt" }] }, - { locations: [{ line: 7, path: "/workspace/late.txt" }] }, - ], - [ - "content", - { content: [{ content: { text: "late", type: "text" }, type: "content" }] }, - { output: [{ text: "late", type: "text" }] }, - ], - [ - "terminal reference", - { content: [{ terminalId: "terminal-late", type: "terminal" }] }, - { terminalItemId: "terminal:terminal-late" }, - ], - ] as const)("enriches a completed tool with a later %s patch", async (_name, patch, expected) => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - kind: "execute", - rawInput: { command: "true" }, - sessionUpdate: "tool_call", - status: "completed", - title: "Run command", - toolCallId: "tool-late", - }), - ); - const endedAt = harness.snapshot().items.find((item) => item.kind === "tool")?.endedAt; - - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - ...patch, - sessionUpdate: "tool_call_update", - toolCallId: "tool-late", - } as SessionNotification["update"]), - ); - - expect(harness.snapshot().items.find((item) => item.kind === "tool")).toMatchObject({ - category: "execute", - endedAt, - input: { command: "true" }, - status: "completed", - title: "Run command", - ...expected, - }); - }); - - test("ignores a completed tool replay that carries no new content", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - kind: "execute", - rawInput: { command: "true" }, - sessionUpdate: "tool_call", - status: "in_progress", - title: "Run command", - toolCallId: "tool-replay", - }), - ); - const completed = notification({ - rawOutput: { exitCode: 0 }, - sessionUpdate: "tool_call_update", - status: "completed", - toolCallId: "tool-replay", - }); - await harness.adapter.handleSessionUpdate(RUN_ID, completed); - const authorityCount = harness.authority.length; - - harness.advance(1_000); - await expect(harness.adapter.handleSessionUpdate(RUN_ID, completed)).resolves.toBeNull(); - - expect(harness.authority).toHaveLength(authorityCount); - expect(harness.snapshot().items.find((item) => item.kind === "tool")).toMatchObject({ - input: { command: "true" }, - status: "completed", - structuredOutput: { exitCode: 0 }, - }); - }); - - test.each(["completed", "failed"] as const)( - "propagates status-only %s tool updates to existing file changes", - async (status) => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: [ - { - newText: "new", - oldText: "old", - path: "/workspace/file.txt", - type: "diff", - }, - ], - sessionUpdate: "tool_call", - title: "Edit", - toolCallId: "tool-1", - }), - ); - await harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - sessionUpdate: "tool_call_update", - status, - toolCallId: "tool-1", - }), - ); - - expect(harness.snapshot().items.find((item) => item.kind === "change")).toMatchObject({ - changes: [ - { - diff: { - type: "json", - value: { newText: "new", oldText: "old" }, - }, - operation: "update", - path: "/workspace/file.txt", - }, - ], - status, - }); - }, - ); - - test("rejects empty permission choices before creating authority state", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - - await expect( - harness.adapter.openPermission(RUN_ID, { - options: [], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Read", toolCallId: "tool-1" }, - }), - ).rejects.toThrow("at least one option"); - expect(harness.snapshot().items).toHaveLength(0); - expect(harness.snapshot().interactions).toHaveLength(0); - }); - - test.each([Number.POSITIVE_INFINITY, 1.5])("rejects invalid timeout %p", (value) => { - expect(() => createHarness(value)).toThrow("finite and positive"); - }); - - test.each([Number.POSITIVE_INFINITY, 1.5])( - "rejects invalid pending permission limit %p", - (value) => { - expect(() => createHarness(5 * 60 * 1_000, undefined, value)).toThrow("finite and positive"); - }, - ); -}); diff --git a/tests/acp-v1-contract-adapter-terminal.test.ts b/tests/acp-v1-contract-adapter-terminal.test.ts deleted file mode 100644 index 90ee4ab..0000000 --- a/tests/acp-v1-contract-adapter-terminal.test.ts +++ /dev/null @@ -1,522 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { - CreateTerminalRequest, - RequestPermissionRequest, - SessionConfigOption, - SessionNotification, -} from "@agentclientprotocol/sdk"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - Run, - SessionSnapshot, -} from "../src/contract"; -import { AcpV1ContractAdapter, toConfigOptions } from "../src/runtimes/acp/v1-contract-adapter"; -import { - type ContractAuthorityUpdate, - type ContractPreviewUpdate, -} from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const NATIVE_SESSION_ID = "native-session-1"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - "interaction.permission": {}, - "item.artifact": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - interactionTimeoutMs = 5 * 60 * 1_000, - previewCheckpointBytes?: number, - maxPendingPermissionBytes?: number, - beforeAuthority?: (update: ContractAuthorityUpdate) => Promise, - afterAuthority?: (update: ContractAuthorityUpdate) => Promise, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - const authority: ContractAuthorityUpdate[] = []; - const committedMutationIds = new Set(); - const previews: ContractPreviewUpdate[] = []; - const commit = (cause: CommittedMutation["cause"], operations: AuthorityOperation[]): void => { - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId: protocolId(1_000 + revision), - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - }; - const adapter = new AcpV1ContractAdapter({ - authority: async (update) => { - await beforeAuthority?.(update); - authority.push(update); - if (!committedMutationIds.has(update.mutationId)) { - committedMutationIds.add(update.mutationId); - commit(update.cause, [...update.operations] as AuthorityOperation[]); - } - try { - await afterAuthority?.(update); - } catch (cause) { - throw new AuthorityOutcomeUnknownError( - cause instanceof Error ? cause.message : "Authority outcome is unknown.", - { cause }, - ); - } - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs, - maxPendingPermissionBytes, - nativeSessionId: NATIVE_SESSION_ID, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - previewCheckpointBytes, - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - previews, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: resolution.value, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -async function registerRun(adapter: AcpV1ContractAdapter): Promise { - adapter.attachRun(activeRun("2026-07-16T08:00:00.000Z")); -} - -function notification(update: SessionNotification["update"]): SessionNotification { - return { sessionId: NATIVE_SESSION_ID, update }; -} - -describe("ACP V1 Contract adapter", () => { - test("rejects a pre-admitted follower without locking out an explicit retry", async () => { - const entered = Promise.withResolvers(); - const release = Promise.withResolvers(); - const mutationIds: string[] = []; - let first = true; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event !== "session/tool_call") { - return; - } - - mutationIds.push(update.mutationId); - if (first) { - first = false; - entered.resolve(); - await release.promise; - throw new AuthorityOutcomeUnknownError("authority result lost"); - } - }); - await registerRun(harness.adapter); - const update = notification({ - sessionUpdate: "tool_call", - status: "in_progress", - title: "Run command", - toolCallId: "tool-follower", - }); - const leader = harness.adapter.handleSessionUpdate(RUN_ID, update); - await entered.promise; - const follower = harness.adapter.handleSessionUpdate(RUN_ID, update); - release.resolve(); - - const settled = await Promise.allSettled([leader, follower]); - expect(settled.map((result) => result.status)).toEqual(["rejected", "rejected"]); - expect( - settled.every( - (result) => - result.status === "rejected" && result.reason instanceof AuthorityOutcomeUnknownError, - ), - ).toBe(true); - expect(mutationIds).toHaveLength(1); - - await harness.adapter.handleSessionUpdate(RUN_ID, update); - expect(mutationIds).toEqual([mutationIds[0], mutationIds[0]]); - }); - - test("coalesces concurrent exact retries of one unknown session update", async () => { - let first = true; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (first && update.event === "session/agent_message_chunk") { - first = false; - throw new AuthorityOutcomeUnknownError("authority result lost"); - } - }); - await registerRun(harness.adapter); - const update = notification({ - content: { text: "x", type: "text" }, - messageId: "message-retry", - sessionUpdate: "agent_message_chunk", - }); - await expect(harness.adapter.handleSessionUpdate(RUN_ID, update)).rejects.toBeInstanceOf( - AuthorityOutcomeUnknownError, - ); - - await Promise.all([ - harness.adapter.handleSessionUpdate(RUN_ID, update), - harness.adapter.handleSessionUpdate(RUN_ID, update), - ]); - await harness.adapter.completePrompt(RUN_ID, { stopReason: "end_turn" }); - - expect(harness.snapshot().items).toContainEqual( - expect.objectContaining({ - content: [{ text: "x", type: "text" }], - id: "message:message-retry", - }), - ); - }); - - test("freezes a permission tool mutation across an unknown exact retry", async () => { - const mutationIds: string[] = []; - let first = true; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event !== "permission/requested.tool") { - return; - } - - mutationIds.push(update.mutationId); - if (first) { - first = false; - throw new AuthorityOutcomeUnknownError("authority result lost"); - } - }); - await registerRun(harness.adapter); - const request = { - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId: "tool-time" }, - } satisfies RequestPermissionRequest; - - await expect(harness.adapter.openPermission(RUN_ID, request)).rejects.toBeInstanceOf( - AuthorityOutcomeUnknownError, - ); - harness.advance(1_000); - await expect(harness.adapter.openPermission(RUN_ID, request)).resolves.toBeDefined(); - - expect(mutationIds).toEqual([mutationIds[0], mutationIds[0]]); - }); - - test("freezes terminal registration across an unknown exact retry", async () => { - const mutationIds: string[] = []; - let first = true; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event !== "terminal/created") { - return; - } - - mutationIds.push(update.mutationId); - if (first) { - first = false; - throw new AuthorityOutcomeUnknownError("authority result lost"); - } - }); - await registerRun(harness.adapter); - const request = { - command: "true", - sessionId: NATIVE_SESSION_ID, - } satisfies CreateTerminalRequest; - - await expect( - harness.adapter.registerTerminal(RUN_ID, "terminal-time", request), - ).rejects.toBeInstanceOf(AuthorityOutcomeUnknownError); - harness.advance(1_000); - await expect(harness.adapter.registerTerminal(RUN_ID, "terminal-time", request)).resolves.toBe( - "terminal:terminal-time", - ); - - expect(mutationIds).toEqual([mutationIds[0], mutationIds[0]]); - }); - - test("drains streamed updates before completing the Run", async () => { - const blocked = Promise.withResolvers(); - const release = Promise.withResolvers(); - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event === "session/agent_message_chunk") { - blocked.resolve(); - await release.promise; - } - }); - await registerRun(harness.adapter); - const update = harness.adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: { text: "last", type: "text" }, - messageId: "message-last", - sessionUpdate: "agent_message_chunk", - }), - ); - await blocked.promise; - let completed = false; - const completion = harness.adapter - .completePrompt(RUN_ID, { stopReason: "end_turn" }) - .then(() => { - completed = true; - }); - await Bun.sleep(0); - const completedBeforeUpdate = completed; - - release.resolve(); - await Promise.all([update, completion]); - expect(completedBeforeUpdate).toBe(false); - expect(harness.snapshot().runs[0]?.status).toBe("completed"); - expect(harness.snapshot().items).toContainEqual( - expect.objectContaining({ - content: [{ text: "last", type: "text" }], - kind: "message", - status: "completed", - }), - ); - }); - - test("drains terminal authority writes before completing the Run", async () => { - const blocked = Promise.withResolvers(); - const release = Promise.withResolvers(); - const harness = createHarness(5 * 60 * 1_000, 5, undefined, async (update) => { - if (update.event === "preview/replace.checkpoint") { - blocked.resolve(); - await release.promise; - } - }); - await registerRun(harness.adapter); - await harness.adapter.registerTerminal(RUN_ID, "terminal-1"); - const output = harness.adapter.handleTerminalOutput(RUN_ID, "terminal-1", { - exitStatus: null, - output: "abcdef", - truncated: false, - }); - await blocked.promise; - let completed = false; - const completion = harness.adapter - .completePrompt(RUN_ID, { stopReason: "end_turn" }) - .then(() => { - completed = true; - }); - await Bun.sleep(0); - - expect(completed).toBe(false); - release.resolve(); - await Promise.all([output, completion]); - expect(harness.snapshot().runs[0]?.status).toBe("completed"); - }); - - test("drains a permission authority write before completing the Run", async () => { - const blocked = Promise.withResolvers(); - const release = Promise.withResolvers(); - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event === "permission/requested") { - blocked.resolve(); - await release.promise; - } - }); - await registerRun(harness.adapter); - const permission = harness.adapter.openPermission(RUN_ID, { - options: [{ kind: "allow_once", name: "Allow", optionId: "yes" }], - sessionId: NATIVE_SESSION_ID, - toolCall: { title: "Run command", toolCallId: "tool-last" }, - }); - await blocked.promise; - let completed = false; - const completion = harness.adapter - .completePrompt(RUN_ID, { stopReason: "end_turn" }) - .then(() => { - completed = true; - }); - await Bun.sleep(0); - - expect(completed).toBe(false); - release.resolve(); - await Promise.all([permission, completion]); - expect(harness.snapshot().runs[0]?.status).toBe("completed"); - expect(harness.snapshot().interactions.every((entry) => entry.status !== "open")).toBe(true); - }); - - test.each([ - [ - "prompt completion", - (adapter: AcpV1ContractAdapter) => adapter.completePrompt(RUN_ID, { stopReason: "end_turn" }), - ], - [ - "session update", - (adapter: AcpV1ContractAdapter) => - adapter.handleSessionUpdate( - RUN_ID, - notification({ - content: { text: "late", type: "text" }, - messageId: "late-message", - sessionUpdate: "agent_message_chunk", - }), - ), - ], - [ - "terminal output", - (adapter: AcpV1ContractAdapter) => - adapter.handleTerminalOutput(RUN_ID, "terminal-late", { - exitStatus: null, - output: "late", - truncated: false, - }), - ], - [ - "terminal exit", - (adapter: AcpV1ContractAdapter) => - adapter.handleTerminalExit(RUN_ID, "terminal-late", { - exitCode: 0, - signal: null, - }), - ], - ] as const)("ignores late %s after the Run is terminal", async (_name, lateEvent) => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.registerTerminal(RUN_ID, "terminal-late"); - await harness.adapter.completePrompt(RUN_ID, { stopReason: "end_turn" }); - const before = harness.authority.length; - - await lateEvent(harness.adapter); - expect(harness.authority).toHaveLength(before); - }); - - test("returns session-scoped updates and rejects the wrong native session", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - const update = { - configOptions: [], - sessionUpdate: "config_option_update", - } satisfies SessionNotification["update"]; - - await expect( - harness.adapter.handleSessionUpdate(RUN_ID, notification(update)), - ).resolves.toEqual(update); - await expect( - harness.adapter.handleSessionUpdate(RUN_ID, { - sessionId: "other-session", - update, - }), - ).rejects.toThrow("active native session"); - }); - - test("maps stable boolean and grouped select options into the Session config", () => { - const options = [ - { - currentValue: true, - id: "auto-approve", - name: "Auto approve", - type: "boolean", - }, - { - currentValue: "fast", - id: "model", - name: "Model", - options: [ - { - group: "recommended", - name: "Recommended", - options: [{ name: "Fast", value: "fast" }], - }, - ], - type: "select", - }, - ] satisfies SessionConfigOption[]; - - expect(toConfigOptions(options)).toEqual([ - { - id: "auto-approve", - label: "Auto approve", - type: "boolean", - value: true, - }, - { - choices: [{ id: "fast", label: "Fast" }], - extensions: { - "agentclientprotocol.v1/select-groups": [ - { id: "recommended", label: "Recommended", optionIds: ["fast"] }, - ], - }, - id: "model", - label: "Model", - type: "select", - value: "fast", - }, - ]); - }); -}); diff --git a/tests/agent-driver-kernel-commands.test.ts b/tests/agent-driver-kernel-commands.test.ts index 0819417..1e0fbbd 100644 --- a/tests/agent-driver-kernel-commands.test.ts +++ b/tests/agent-driver-kernel-commands.test.ts @@ -1,10 +1,12 @@ import { describe, expect, test } from "bun:test"; +import { ACTIVE_TURN_CANCEL_GRACE_MS } from "../src/core/driver-command-dispatcher"; import type { AgentDriverContext, AgentDriverContextPortOverrides, } from "../src/core/agent-driver-backend"; import { AgentDriverKernelCore } from "../src/core/agent-driver-kernel"; +import { DRIVER_EVENT_DELIVERY_TIMEOUT_MS } from "../src/core/driver-runtime-io"; import type { DriverEventInput } from "../src/protocol/events"; import type { RuntimeCommand } from "../src/runtime-command"; import { settlePromiseWithTimeout } from "../src/utils/async"; @@ -239,6 +241,7 @@ describe("AgentDriverKernelCore", () => { entered.resolve(); await late.promise; }, + currentRunId: () => null, pushEvents: async ({ events }) => ({ accepted: events.map((event, index) => ({ seq: index + 1, @@ -632,17 +635,32 @@ describe("AgentDriverKernelCore", () => { }) .catch(() => {}); await inputEntered.promise; + const nativeSetTimeout = globalThis.setTimeout; + const acceleratedSetTimeout = ( + callback: (...args: unknown[]) => void, + timeout?: number, + ...args: unknown[] + ) => + nativeSetTimeout( + callback, + timeout === ACTIVE_TURN_CANCEL_GRACE_MS + DRIVER_EVENT_DELIVERY_TIMEOUT_MS ? 10 : timeout, + ...args, + ); + globalThis.setTimeout = acceleratedSetTimeout as typeof setTimeout; - const first = await Promise.allSettled([kernel.stop("first stop")]); - const second = await Promise.allSettled([kernel.stop("second stop")]); - const settledBeforeRelease = inputSettled; - releaseInput.resolve(); - await input; + try { + const first = await Promise.allSettled([kernel.stop("first stop")]); + const second = await Promise.allSettled([kernel.stop("second stop")]); - expect(first[0]?.status).toBe("rejected"); - expect(second[0]?.status).toBe("rejected"); - expect(settledBeforeRelease).toBe(false); - }, 12_000); + expect(first[0]?.status).toBe("rejected"); + expect(second[0]?.status).toBe("rejected"); + expect(inputSettled).toBe(false); + } finally { + globalThis.setTimeout = nativeSetTimeout; + releaseInput.resolve(); + await input; + } + }); test("keeps cleanup event delivery available across a transient stop failure", async () => { const backend = createBackend(); diff --git a/tests/architecture-boundaries.test.ts b/tests/architecture-boundaries.test.ts index 69d09a5..f3b9ab5 100644 --- a/tests/architecture-boundaries.test.ts +++ b/tests/architecture-boundaries.test.ts @@ -1,11 +1,33 @@ import { describe, expect, test } from "bun:test"; -import { readFileSync } from "node:fs"; -import { relative, resolve, sep } from "node:path"; +import { dirname, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; -import * as ts from "typescript"; +import { API, type Project } from "typescript/unstable/async"; +import { + SyntaxKind, + type CallExpression, + type Node, + type SourceFile, +} from "typescript/unstable/ast"; +import { + isCallExpression, + isElementAccessExpression, + isExportAssignment, + isExportDeclaration, + isFunctionDeclaration, + isIdentifier, + isImportDeclaration, + isImportEqualsDeclaration, + isMetaProperty, + isMethodSignatureDeclaration, + isNamedExports, + isNamedImports, + isPropertyAccessExpression, + isStringLiteralLikeNode, +} from "typescript/unstable/ast/is"; const repositoryRoot = fileURLToPath(new URL("../", import.meta.url)); +const packageManifestPath = resolve(repositoryRoot, "package.json"); const sourceRoot = resolve(repositoryRoot, "src"); interface SourceGraph { @@ -13,91 +35,351 @@ interface SourceGraph { readonly files: readonly string[]; } -function loadCompilerOptions(): ts.CompilerOptions { - const configPath = resolve(repositoryRoot, "tsconfig.json"); - const config = ts.readConfigFile(configPath, (path) => readFileSync(path, "utf8")); +interface ArchitectureAudit { + readonly graph: SourceGraph; + readonly resolutionDependencies: ReadonlySet; + readonly resolutionFixtureError: string | undefined; + readonly runtimeLoaderFixtureErrors: ReadonlyMap; + readonly unsupportedFixtureErrors: readonly (string | undefined)[]; +} + +function sourcePath(path: string): string { + return relative(repositoryRoot, path).split(sep).join("/"); +} + +function isImportMetaRequire(node: Node): boolean { + if (!isPropertyAccessExpression(node) && !isElementAccessExpression(node)) { + return false; + } + + if ( + !isMetaProperty(node.expression) || + node.expression.keywordToken !== SyntaxKind.ImportKeyword + ) { + return false; + } - if (config.error !== undefined) { - throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, "\n")); + if (isPropertyAccessExpression(node)) { + return node.name.text === "require"; } - return ts.parseJsonConfigFileContent(config.config, ts.sys, repositoryRoot).options; + return ( + isStringLiteralLikeNode(node.argumentExpression) && node.argumentExpression.text === "require" + ); } -function moduleSpecifiers(sourceFile: ts.SourceFile): string[] { - const specifiers: string[] = []; +function isTypeOnlyModuleDeclaration(node: Node): boolean { + if (isImportDeclaration(node)) { + const clause = node.importClause; + + return ( + clause?.phaseModifier === SyntaxKind.TypeKeyword || + (clause?.name === undefined && + clause?.namedBindings !== undefined && + isNamedImports(clause.namedBindings) && + clause.namedBindings.elements.length > 0 && + clause.namedBindings.elements.every((element) => element.isTypeOnly)) + ); + } + + return ( + isExportDeclaration(node) && + (node.isTypeOnly || + (node.exportClause !== undefined && + isNamedExports(node.exportClause) && + node.exportClause.elements.length > 0 && + node.exportClause.elements.every((element) => element.isTypeOnly))) + ); +} + +async function assertStaticEsm(project: Project, file: string, root: Node): Promise { + const commonJsIdentifiers: Node[] = []; + const calls: CallExpression[] = []; + + function visit(node: Node): void { + if (isCallExpression(node)) { + if (node.expression.kind === SyntaxKind.ImportKeyword) { + throw new Error(`Dynamic imports are unsupported: ${sourcePath(file)}.`); + } + + calls.push(node); + } + + if (isImportMetaRequire(node)) { + throw new Error(`Runtime module loaders are unsupported: ${sourcePath(file)}.`); + } - function visit(node: ts.Node): void { if ( - (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && + (isImportDeclaration(node) || isExportDeclaration(node)) && + !isTypeOnlyModuleDeclaration(node) && node.moduleSpecifier !== undefined && - ts.isStringLiteralLike(node.moduleSpecifier) + isStringLiteralLikeNode(node.moduleSpecifier) && + ["module", "node:module"].includes(node.moduleSpecifier.text) ) { - specifiers.push(node.moduleSpecifier.text); - } else if ( - ts.isCallExpression(node) && - node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.length === 1 && - ts.isStringLiteralLike(node.arguments[0]!) + throw new Error(`Runtime module loaders are unsupported: ${sourcePath(file)}.`); + } + + if (isImportEqualsDeclaration(node) || (isExportAssignment(node) && node.isExportEquals)) { + throw new Error(`CommonJS syntax is unsupported: ${sourcePath(file)}.`); + } + + if ( + isIdentifier(node) && + ["exports", "module", "require"].includes(node.text) && + !(isPropertyAccessExpression(node.parent) && node.parent.name === node) ) { - specifiers.push(node.arguments[0].text); + commonJsIdentifiers.push(node); } - ts.forEachChild(node, visit); + node.forEachChild(visit); } - visit(sourceFile); - return specifiers; + visit(root); + const symbols = await project.checker.getSymbolAtLocation(commonJsIdentifiers); + const sourceFile = root.getSourceFile().fileName; + + for (const symbol of symbols) { + if ( + symbol === undefined || + (await project.checker.isUnknownSymbol(symbol)) || + !symbol.declarations.some(({ path }) => path === sourceFile) + ) { + throw new Error(`CommonJS syntax is unsupported: ${sourcePath(file)}.`); + } + } + + for (const call of calls) { + const declaration = (await project.checker.getResolvedSignature(call))?.declaration; + + if ( + declaration?.path.endsWith(`${sep}@types${sep}node${sep}process.d.ts`) && + declaration.kind === SyntaxKind.MethodSignature + ) { + const node = await declaration.resolve(project); + + if ( + node !== undefined && + isMethodSignatureDeclaration(node) && + isIdentifier(node.name) && + node.name.text === "getBuiltinModule" + ) { + throw new Error(`Runtime module loaders are unsupported: ${sourcePath(file)}.`); + } + } + } } -function isSourceFile(path: string): boolean { - return path === sourceRoot || path.startsWith(`${sourceRoot}${sep}`); +async function captureError(check: () => Promise): Promise { + try { + await check(); + return undefined; + } catch (error) { + return error instanceof Error ? error.message : String(error); + } } -function createSourceGraph(): SourceGraph { - const options = loadCompilerOptions(); - const config = ts.parseJsonConfigFileContent( - ts.readConfigFile(resolve(repositoryRoot, "tsconfig.json"), (path) => - readFileSync(path, "utf8"), - ).config, - ts.sys, - repositoryRoot, +function isWorkspaceSpecifier(specifier: string): boolean { + return ( + specifier.startsWith(".") || + specifier.startsWith("/") || + specifier.startsWith("#") || + specifier.startsWith("file:") ); - const program = ts.createProgram(config.fileNames, options); - const files = program - .getSourceFiles() - .map((sourceFile) => sourceFile.fileName) - .filter((path) => isSourceFile(path) && !path.endsWith(".d.ts")) - .toSorted(); - const fileSet = new Set(files); - const dependencies = new Map>(); +} + +function isDependencyPath(path: string): boolean { + return path.includes(`${sep}node_modules${sep}`); +} + +function admitSourceDependency( + importer: string, + specifier: string, + target: string, + files: ReadonlySet, +): string | undefined { + if (target === packageManifestPath) { + return undefined; + } + + if (target.startsWith(`${sourceRoot}${sep}`)) { + if (!files.has(target)) { + throw new Error(`Source import resolved to an unsupported file: ${sourcePath(target)}.`); + } + + return target; + } + + if (!isWorkspaceSpecifier(specifier) && isDependencyPath(target)) { + return undefined; + } + + throw new Error( + `Source import resolves outside the source root: ${sourcePath(importer)} -> ${specifier}.`, + ); +} + +async function resolveModuleDependencies( + project: Project, + importer: string, + source: SourceFile, + files: ReadonlySet, +): Promise> { + const dependencies = new Set(); + const moduleSpecifiers = [ + ...source.imports, + ...source.moduleAugmentations.filter((node) => !(isIdentifier(node) && node.text === "global")), + ]; + const symbols = await project.checker.getSymbolAtLocation(moduleSpecifiers); + + for (const [index, node] of moduleSpecifiers.entries()) { + if (!isStringLiteralLikeNode(node)) { + throw new Error(`Source import is not statically auditable: ${sourcePath(importer)}.`); + } - for (const file of files) { - const sourceFile = program.getSourceFile(file); + const symbol = symbols[index]; - if (sourceFile === undefined) { - throw new Error(`TypeScript program omitted ${file}.`); + if (symbol === undefined || (await project.checker.isUnknownSymbol(symbol))) { + throw new Error(`Source import cannot be resolved: ${sourcePath(importer)} -> ${node.text}.`); } - const resolvedDependencies = new Set(); + const declarations = symbol.declarations.filter(({ kind }) => kind === SyntaxKind.SourceFile); + + if (declarations.length === 0) { + if ( + !isWorkspaceSpecifier(node.text) && + symbol.declarations.length > 0 && + symbol.declarations.every(({ path }) => isDependencyPath(path)) + ) { + continue; + } + + throw new Error( + `Source import has no file declaration: ${sourcePath(importer)} -> ${node.text}.`, + ); + } - for (const specifier of moduleSpecifiers(sourceFile)) { - const resolvedModule = ts.resolveModuleName(specifier, file, options, ts.sys).resolvedModule; - const dependency = resolvedModule?.resolvedFileName; + for (const declaration of declarations) { + const dependency = admitSourceDependency(importer, node.text, declaration.path, files); - if (dependency !== undefined && fileSet.has(dependency)) { - resolvedDependencies.add(dependency); + if (dependency !== undefined) { + dependencies.add(dependency); } } + } + + for (const reference of source.referencedFiles) { + const target = await project.program.getSourceFile( + resolve(dirname(importer), reference.fileName), + ); + + if (target === undefined) { + throw new Error( + `Source reference cannot be resolved: ${sourcePath(importer)} -> ${reference.fileName}.`, + ); + } - dependencies.set(file, resolvedDependencies); + const dependency = admitSourceDependency(importer, reference.fileName, target.fileName, files); + + if (dependency !== undefined) { + dependencies.add(dependency); + } } - return { dependencies, files }; + return dependencies; } -function sourcePath(path: string): string { - return relative(repositoryRoot, path).split(sep).join("/"); +async function getFixture(project: Project, name: string): Promise { + const fixture = await project.program.getSourceFile( + resolve(repositoryRoot, `tests/fixtures/${name}.ts`), + ); + + if (fixture === undefined) { + throw new Error(`TypeScript did not load the ${name} fixture.`); + } + + return fixture; +} + +async function createArchitectureAudit(): Promise { + const api = new API({ cwd: repositoryRoot }); + const configPath = resolve(repositoryRoot, "tsconfig.json"); + + try { + const snapshot = await api.updateSnapshot({ openProjects: [configPath] }); + const project = snapshot.getProject(configPath); + + if (project === undefined) { + throw new Error(`TypeScript project is missing: ${configPath}.`); + } + + const files = (await project.program.getSourceFileNames()) + .filter( + (path) => path.startsWith(`${sourceRoot}${sep}`) && !/\.d\.(?:cts|mts|ts)$/.test(path), + ) + .toSorted(); + const fileSet = new Set(files); + const dependencies = new Map>(); + + await Promise.all( + files.map(async (file) => { + const source = await project.program.getSourceFile(file); + + if (source === undefined) { + throw new Error(`TypeScript did not load ${sourcePath(file)}.`); + } + + await assertStaticEsm(project, file, source); + dependencies.set(file, await resolveModuleDependencies(project, file, source, fileSet)); + }), + ); + + const [auditFixture, resolutionFixture, runtimeLoaderFixture] = await Promise.all([ + getFixture(project, "architecture-boundaries-audit"), + getFixture(project, "architecture-boundaries-resolution"), + getFixture(project, "architecture-boundaries-runtime-loaders"), + ]); + const resolutionDependencies = await resolveModuleDependencies( + project, + resolutionFixture.fileName, + resolutionFixture, + fileSet, + ); + const unsupportedFixtureErrors = await Promise.all( + auditFixture.statements + .slice(1) + .map((statement) => + captureError(() => assertStaticEsm(project, auditFixture.fileName, statement)), + ), + ); + const resolutionFixtureError = await captureError(() => + assertStaticEsm(project, resolutionFixture.fileName, resolutionFixture), + ); + const runtimeLoaderFixtureErrors = new Map( + await Promise.all( + runtimeLoaderFixture.statements + .filter(isFunctionDeclaration) + .map( + async (statement) => + [ + statement.name!.text, + await captureError(() => + assertStaticEsm(project, runtimeLoaderFixture.fileName, statement), + ), + ] as const, + ), + ), + ); + + return { + graph: { dependencies, files }, + resolutionDependencies, + resolutionFixtureError, + runtimeLoaderFixtureErrors, + unsupportedFixtureErrors, + }; + } finally { + await api.close(); + } } function isWithin(path: string, directory: string): boolean { @@ -115,17 +397,16 @@ function boundaryViolations(graph: SourceGraph): string[] { const violations: string[] = []; for (const file of graph.files) { - for (const dependency of graph.dependencies.get(file) ?? []) { + for (const dependency of reachableDependencies(graph, file)) { const forbidden = (isWithin(file, "contract") && ["core", "runtimes", "infrastructure", "stores", "surfaces"].some((directory) => isWithin(dependency, directory), )) || (isWithin(file, "protocol") && - (["core", "runtimes", "infrastructure", "runtime-events"].some((directory) => + ["core", "runtimes", "infrastructure", "runtime-events"].some((directory) => isWithin(dependency, directory), - ) || - isWithin(dependency, "runtime-events"))) || + )) || (isWithin(file, "core") && (isProviderRuntime(dependency) || ["infrastructure", "stores", "surfaces"].some((directory) => @@ -145,11 +426,29 @@ function boundaryViolations(graph: SourceGraph): string[] { return violations.toSorted(); } +function reachableDependencies(graph: SourceGraph, file: string): ReadonlySet { + const reachable = new Set(); + const pending = [...(graph.dependencies.get(file) ?? [])]; + + while (pending.length > 0) { + const dependency = pending.pop()!; + + if (reachable.has(dependency)) { + continue; + } + + reachable.add(dependency); + pending.push(...(graph.dependencies.get(dependency) ?? [])); + } + + return reachable; +} + function compositionViolations(graph: SourceGraph): string[] { return graph.files .filter((file) => !isWithin(file, "bin")) .filter((file) => { - const dependencies = [...(graph.dependencies.get(file) ?? [])]; + const dependencies = [...reachableDependencies(graph, file)]; return ( dependencies.some((dependency) => isWithin(dependency, "core")) && dependencies.some(isProviderRuntime) && @@ -228,15 +527,87 @@ function bidirectionalDirectories(graph: SourceGraph): string[] { return [...pairs].toSorted(); } -const graph = createSourceGraph(); +const audit = await createArchitectureAudit(); +const graph = audit.graph; describe("source architecture", () => { + test("uses TypeScript resolution for every dependency syntax", () => { + expect(audit.resolutionDependencies).toEqual( + new Set([ + resolve(sourceRoot, "contract/index.ts"), + resolve(sourceRoot, "core/agent-driver-kernel.ts"), + resolve(sourceRoot, "core/driver-runtime-state.ts"), + resolve(sourceRoot, "paths.ts"), + ]), + ); + }); + + test("allows only the exact root package manifest outside the source root", () => { + const importer = resolve(sourceRoot, "runtime.ts"); + const files = new Set(graph.files); + + expect(admitSourceDependency(importer, "../package.json", packageManifestPath, files)).toBe( + undefined, + ); + expect(() => + admitSourceDependency(importer, "../README.md", resolve(repositoryRoot, "README.md"), files), + ).toThrow("outside the source root"); + }); + + test("rejects runtime-only module loading without rejecting static ESM or local names", () => { + for (const error of audit.unsupportedFixtureErrors) { + expect(error).toMatch( + /(?:CommonJS syntax is|Dynamic imports are|Runtime module loaders are) unsupported/, + ); + } + expect(audit.resolutionFixtureError).toBeUndefined(); + for (const name of ["computed", "destructured", "direct", "imported"]) { + expect(audit.runtimeLoaderFixtureErrors.get(name)).toContain( + "Runtime module loaders are unsupported", + ); + } + expect(audit.runtimeLoaderFixtureErrors.get("local")).toBeUndefined(); + }); + test("keeps contract, protocol, core, and provider imports pointing inward", () => { expect(boundaryViolations(graph)).toEqual([]); + + const core = resolve(sourceRoot, "core/audit.ts"); + const wrapper = resolve(sourceRoot, "audit/wrapper.ts"); + const provider = resolve(sourceRoot, "runtimes/openai/audit.ts"); + const files = [core, wrapper, provider]; + const dependencies = new Map>([ + [core, new Set([wrapper])], + [wrapper, new Set([provider])], + [provider, new Set()], + ]); + + expect(boundaryViolations({ dependencies, files })).toEqual([ + "src/core/audit.ts -> src/runtimes/openai/audit.ts", + ]); }); - test("keeps bin as the only full runtime composition root", () => { + test("keeps bin as the only transitive full runtime composition root", () => { expect(compositionViolations(graph)).toEqual([]); + + const root = resolve(sourceRoot, "composition-audit.ts"); + const wrapper = resolve(sourceRoot, "audit/wrapper.ts"); + const core = resolve(sourceRoot, "core/audit.ts"); + const provider = resolve(sourceRoot, "runtimes/openai/audit.ts"); + const infrastructure = resolve(sourceRoot, "infrastructure/audit.ts"); + const files = [root, wrapper, core, provider, infrastructure]; + const dependencies = new Map>([ + [root, new Set([wrapper])], + [wrapper, new Set([core, provider, infrastructure])], + [core, new Set()], + [provider, new Set()], + [infrastructure, new Set()], + ]); + + expect(compositionViolations({ dependencies, files })).toEqual([ + "src/audit/wrapper.ts", + "src/composition-audit.ts", + ]); }); test("has no file-level import cycles", () => { diff --git a/tests/claude-agent-sdk-driver-backend.test.ts b/tests/claude-agent-sdk-driver-backend.test.ts index 75887e6..b3eb397 100644 --- a/tests/claude-agent-sdk-driver-backend.test.ts +++ b/tests/claude-agent-sdk-driver-backend.test.ts @@ -10,6 +10,7 @@ import type { import { DriverTurnCancelledError } from "../src/core/driver-runtime-state"; import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; +import type { RunId } from "../src/protocol/id"; import type { DriverStartInput } from "../src/protocol/start"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { ClaudeAgentSdkDriverBackend } from "../src/runtimes/claude/agent-sdk-driver-backend"; @@ -19,6 +20,10 @@ import { bootPayload, DRIVER_TEST_IDS } from "./driver-runtime-boundary-fixtures const PREWARM_ENV = "AGENT_DRIVER_CLAUDE_PREWARM"; const previousPrewarm = process.env[PREWARM_ENV]; +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + afterEach(() => { if (previousPrewarm === undefined) { delete process.env[PREWARM_ENV]; @@ -112,6 +117,7 @@ function createHarness( payloadOverride?: DriverStartInput, ) { const events: DriverEventInput[] = []; + let currentRunId: RunId | null = null; let seq = 0; const logger = createBufferedSinkLogger({ level: "error", @@ -127,9 +133,22 @@ function createHarness( } as DriverStartInput); const context = createAgentDriverContext({ eventSink: { + currentRunId: () => currentRunId, pushEvents: async ({ events: batch }) => { await beforePush?.(batch); events.push(...batch); + for (const event of batch) { + if (event.kind === "run.started" && event.runId !== undefined) { + currentRunId = event.runId; + } else if ( + (event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed") && + event.runId === currentRunId + ) { + currentRunId = null; + } + } return { accepted: batch.map((event) => ({ seq: (seq += 1), type: event.kind })), }; @@ -150,6 +169,63 @@ function createHarness( } describe("Claude Agent SDK driver backend", () => { + test("rejects invalid native session IDs before retaining or publishing them", async () => { + const oversizedSessionId = "s".repeat(257); + const resumePayload = { + ...bootPayload, + execution: { + ...bootPayload.execution, + session: { + ...bootPayload.execution.session, + nativeResumeRef: { + kind: "claude_session_id", + runtimeId: "claude-agent-sdk", + value: oversizedSessionId, + }, + }, + }, + runtime: "claude-agent-sdk", + runtimeTransport: "claude-agent-sdk", + } as DriverStartInput; + expect(() => new ClaudeAgentSdkDriverBackend(resumePayload)).toThrow( + "Claude native session ID must contain 1-256 UTF-8 bytes (received 257).", + ); + + const harness = createHarness({ + createQueryOptions: async () => ({}), + query: () => fakeQuery([resultMessage(oversizedSessionId)]), + startup: async () => { + throw new Error("prewarm is disabled"); + }, + }); + await expect( + harness.backend.handleInput(harness.context, { text: "hello" }, DRIVER_TEST_IDS.runId), + ).rejects.toThrow("Claude native session ID must contain 1-256 UTF-8 bytes (received 257)."); + expect(harness.events.some((event) => event.kind === "runtime.resume.updated")).toBe(false); + expect(harness.events.some((event) => event.kind === "run.failed")).toBe(true); + expect(JSON.stringify(harness.events)).not.toContain(oversizedSessionId); + await harness.logger.destroy(); + + const emptyHarness = createHarness({ + createQueryOptions: async () => ({}), + query: () => fakeQuery([resultMessage("")]), + startup: async () => { + throw new Error("prewarm is disabled"); + }, + }); + await expect( + emptyHarness.backend.handleInput( + emptyHarness.context, + { text: "hello" }, + DRIVER_TEST_IDS.runId, + ), + ).rejects.toThrow("Claude native session ID must contain 1-256 UTF-8 bytes (received 0)."); + expect(emptyHarness.events.some((event) => event.kind === "runtime.resume.updated")).toBe( + false, + ); + await emptyHarness.logger.destroy(); + }); + test("consumes a ready prewarm for the first turn", async () => { process.env[PREWARM_ENV] = "1"; const startupCalled = Promise.withResolvers(); @@ -1183,7 +1259,6 @@ describe("Claude Agent SDK driver backend", () => { "does not replace a selected $terminal provider terminal after its delivery fails", async ({ message, terminal }) => { delete process.env[PREWARM_ENV]; - let rejectTerminal = true; const terminalAttempts: string[][] = []; const harness = createHarness( { @@ -1202,8 +1277,7 @@ describe("Claude Agent SDK driver backend", () => { if (terminals.length > 0) { terminalAttempts.push(terminals); } - if (rejectTerminal && terminals.includes(terminal)) { - rejectTerminal = false; + if (terminals.includes(terminal)) { throw new Error("terminal delivery unavailable"); } }, @@ -1215,7 +1289,7 @@ describe("Claude Agent SDK driver backend", () => { await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); await harness.logger.destroy(); - expect(terminalAttempts).toEqual([[terminal]]); + expect(terminalAttempts).toEqual([[terminal], [terminal]]); }, ); @@ -1283,6 +1357,67 @@ describe("Claude Agent SDK driver backend", () => { ]); }); + test("adopts a conversation reset as the next native resume session", async () => { + delete process.env[PREWARM_ENV]; + let queryIndex = 0; + const optionSessionIds: Array = []; + const harness = createHarness({ + createQueryOptions: async (input) => { + optionSessionIds.push(input.nativeSessionId); + return {}; + }, + query: () => { + queryIndex += 1; + return queryIndex === 1 + ? fakeQuery([resultMessage("native-session-1")]) + : fakeQuery([ + { + event: { message: { id: "old-message" }, type: "message_start" }, + parent_tool_use_id: null, + session_id: "native-session-1", + type: "stream_event", + uuid: "old-stream", + } as unknown as SDKMessage, + { + new_conversation_id: "native-session-2", + session_id: "native-session-1", + type: "conversation_reset", + uuid: "reset-1", + } as unknown as SDKMessage, + resultMessage("native-session-2"), + ]); + }, + startup: async () => { + throw new Error("prewarm is disabled"); + }, + }); + + await harness.backend.handleInput(harness.context, { text: "first" }, DRIVER_TEST_IDS.runId); + await harness.backend.handleInput( + harness.context, + { text: "second" }, + DRIVER_TEST_IDS.secondRunId, + ); + await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); + await harness.logger.destroy(); + + expect(optionSessionIds).toEqual([null, "native-session-1"]); + expect( + harness.events.flatMap((event) => { + if ( + event.kind !== "runtime.resume.updated" || + !isRecord(event.payload) || + typeof event.payload["resumePointer"] !== "string" + ) { + return []; + } + return [event.payload["resumePointer"]]; + }), + ).toEqual(["native-session-1", "native-session-2"]); + expect(harness.events.some(({ kind }) => kind === "message.cancelled")).toBe(true); + expect(harness.events.filter(({ kind }) => kind === "run.completed")).toHaveLength(2); + }); + test("requires one result frame and ignores provider frames after it", async () => { delete process.env[PREWARM_ENV]; let queryIndex = 0; diff --git a/tests/claude-agent-sdk-durability.test.ts b/tests/claude-agent-sdk-durability.test.ts new file mode 100644 index 0000000..390326e --- /dev/null +++ b/tests/claude-agent-sdk-durability.test.ts @@ -0,0 +1,741 @@ +import { describe, expect, test } from "bun:test"; + +import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; + +import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-event-envelope"; +import { createBufferedSinkLogger } from "../src/observability"; +import type { DriverBootPayload } from "../src/protocol/boot"; +import type { DriverEventInput } from "../src/protocol/events"; +import { createDriverId } from "../src/protocol/id"; +import type { EventId, RunId, SessionId } from "../src/protocol/id"; +import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; +import { toRuntimeEventInput } from "../src/runtime-events"; +import { createAgentDriverContext } from "../src/core/agent-driver-backend"; +import { ClaudeDurableEventTooLargeError } from "../src/runtimes/claude/agent-sdk-event-writer"; +import { ClaudeAgentSdkMessageTranslator } from "../src/runtimes/claude/agent-sdk-message-translator"; +import { ClaudePublicToolCallIdState } from "../src/runtimes/claude/agent-sdk-tool-id"; +import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; +import { CMA_MAX_EVENT_BYTES, encodeCmaSseRecord } from "../src/stores/cma-store"; +import { createCmaMemoryStore } from "../src/stores/memory"; +import { + DRIVER_TEST_IDS, + driverBootPayload, + driverStartInput as bootPayload, +} from "./driver-boot-payload-fixture"; +import { isRecord, messageText } from "./claude-agent-sdk-test-helpers"; + +function payload(event: DriverEventInput): Record { + return isRecord(event.payload) ? event.payload : {}; +} + +function successResult(input: { + readonly result?: string; + readonly structuredOutput?: unknown; +}): SDKMessage { + return { + is_error: false, + modelUsage: {}, + permission_denials: [], + result: input.result ?? "", + ...(input.structuredOutput === undefined ? {} : { structured_output: input.structuredOutput }), + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: createDriverId(), + } as unknown as SDKMessage; +} + +function createCmaHarness() { + const runId = createDriverId() as RunId; + const sessionId = createDriverId() as SessionId; + const events: DriverEventInput[] = []; + const sseFrameBytes: number[] = []; + const logger = createBufferedSinkLogger({ + level: "debug", + service: "claude-agent-sdk-durability-test", + sink: async () => {}, + }); + const context = createAgentDriverContext({ + eventSink: { + currentRunId: () => runId, + pushEvents: async () => ({ accepted: [] }), + }, + logger, + payload: bootPayload, + permission: { request: async () => "allow_once" }, + }); + const store = createCmaMemoryStore({ sessions: [{ id: sessionId }] }); + const append = async (batch: readonly DriverEventInput[]) => { + for (const event of batch) { + events.push(event); + const [envelope] = toRuntimeEventInput( + { + createId: () => createDriverId() as EventId, + occurredAt: "2026-08-13T00:00:00.000Z", + runId, + sessionId, + }, + event, + ); + const records = await store.appendDriverEvent(sessionId, envelope!); + sseFrameBytes.push(...records.map((record) => encodeCmaSseRecord(record).byteLength)); + } + }; + const toolCallIds = new ClaudePublicToolCallIdState(); + const translator = new ClaudeAgentSdkMessageTranslator({ + publicToolCallId: (nativeToolCallId) => toolCallIds.publicId(nativeToolCallId), + push: async (_context, _reason, batch) => append(batch), + pushTerminal: async (_context, _reason, closures, terminal) => append([...closures, terminal]), + recordNativeSessionId: async () => {}, + replaceNativeSessionId: async () => {}, + }); + + return { context, events, logger, runId, sseFrameBytes, translator }; +} + +describe("Claude Agent SDK durable event boundaries", () => { + test("materializes a large result fallback as bounded lossless message chunks", async () => { + const harness = createCmaHarness(); + const text = `开始😀${"x".repeat(1_200_000)}结束`; + + await harness.translator.handleSdkMessage( + harness.context, + successResult({ result: text }), + harness.runId, + ); + + const terminal = harness.events.find((event) => event.kind === "run.completed"); + const finalMessageId = payload(terminal!)["finalMessageId"]; + const snapshots = harness.events.filter( + (event) => event.kind === "message.added" || event.kind === "message.delta", + ); + + expect(typeof finalMessageId).toBe("string"); + expect(snapshots.length).toBeGreaterThan(1); + expect(snapshots.every((event) => event.delivery !== "best_effort")).toBe(true); + expect(messageText(harness.events, finalMessageId as string)).toBe(text); + expect(payload(terminal!)).not.toHaveProperty("finalMessageText"); + expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await harness.logger.destroy(); + }); + + test("rejects oversized structured output before choosing completed closures", async () => { + const harness = createCmaHarness(); + + await harness.translator.handleSdkMessage( + harness.context, + { + event: { message: { id: "native-open" }, type: "message_start" }, + type: "stream_event", + uuid: "wire-open", + } as unknown as SDKMessage, + harness.runId, + ); + await harness.translator.handleSdkMessage( + harness.context, + successResult({ structuredOutput: { data: "x".repeat(600_000) } }), + harness.runId, + ); + + const failed = harness.events.find((event) => event.kind === "run.failed"); + expect(payload(failed!)["error"]).toMatchObject({ + code: "claude.structured_output_too_large", + }); + expect(harness.events.map(({ kind }) => kind)).toContain("message.failed"); + expect(harness.events.map(({ kind }) => kind)).not.toContain("run.completed"); + expect(harness.events.map(({ kind }) => kind)).not.toContain("message.completed"); + expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await harness.logger.destroy(); + }); + + test("keeps accepted structured output inside the real CMA and SSE boundary", async () => { + const harness = createCmaHarness(); + + await harness.translator.handleSdkMessage( + harness.context, + successResult({ structuredOutput: { data: "x".repeat(400_000) } }), + harness.runId, + ); + + expect(harness.events.map(({ kind }) => kind)).toContain("run.completed"); + expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await harness.logger.destroy(); + }); + + test("bounds oversized provider errors before terminal delivery", async () => { + const harness = createCmaHarness(); + const providerError = "x".repeat(1_100_000); + + await harness.translator.handleSdkMessage( + harness.context, + { + errors: [providerError], + is_error: true, + modelUsage: {}, + permission_denials: [], + subtype: "error_during_execution", + terminal_reason: "model_error", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: createDriverId(), + } as unknown as SDKMessage, + harness.runId, + ); + + const failed = harness.events.find((event) => event.kind === "run.failed"); + expect(payload(failed!)["error"]).toMatchObject({ + code: "claude.error_during_execution", + details: { originalMessageUtf8Bytes: 1_100_000 }, + message: "Claude Agent SDK failure exceeded durable event capacity.", + }); + expect(JSON.stringify(failed)).not.toContain(providerError); + expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await harness.logger.destroy(); + }); + + test("bounds oversized cancellation reasons before terminal delivery", async () => { + const harness = createCmaHarness(); + const reason = "x".repeat(1_100_000); + + await harness.translator.cancelTurn(harness.context, harness.runId, reason); + + const cancelled = harness.events.find((event) => event.kind === "run.cancelled"); + expect(payload(cancelled!)).toMatchObject({ + originalReasonUtf8Bytes: 1_100_000, + reason: "Claude cancellation reason exceeded durable event capacity.", + stopReason: "cancelled", + }); + expect(JSON.stringify(cancelled)).not.toContain(reason); + expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await harness.logger.destroy(); + }); + + test("stores one copy of a large tool result and fails closed on oversized structured output", async () => { + const accepted = createCmaHarness(); + const content = "x".repeat(525_000); + + await accepted.translator.handleSdkMessage( + accepted.context, + { + message: { + content: [{ id: "tool-large", input: {}, name: "Read", type: "tool_use" }], + id: "assistant-tool-large", + }, + type: "assistant", + uuid: "wire-tool-large", + } as unknown as SDKMessage, + accepted.runId, + ); + await accepted.translator.handleSdkMessage( + accepted.context, + { + message: { + content: [{ content, tool_use_id: "tool-large", type: "tool_result" }], + }, + type: "user", + uuid: "wire-tool-large-result", + } as unknown as SDKMessage, + accepted.runId, + ); + + const acceptedResult = accepted.events.find( + (event) => event.kind === "tool.call.updated" && payload(event)["content"] === content, + ); + expect(acceptedResult).toBeDefined(); + expect(payload(acceptedResult!)).not.toHaveProperty("rawOutput"); + expect(accepted.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await accepted.logger.destroy(); + + const rejected = createCmaHarness(); + await rejected.translator.handleSdkMessage( + rejected.context, + { + message: { + content: [{ id: "tool-oversized", input: {}, name: "Read", type: "tool_use" }], + id: "assistant-tool-oversized", + }, + type: "assistant", + uuid: "wire-tool-oversized", + } as unknown as SDKMessage, + rejected.runId, + ); + + let failure: ClaudeDurableEventTooLargeError | null = null; + try { + await rejected.translator.handleSdkMessage( + rejected.context, + { + message: { + content: [{ content: "ok", tool_use_id: "tool-oversized", type: "tool_result" }], + }, + tool_use_result: { data: "x".repeat(1_048_000) }, + type: "user", + uuid: "wire-tool-oversized-result", + } as unknown as SDKMessage, + rejected.runId, + ); + } catch (error) { + if (error instanceof ClaudeDurableEventTooLargeError) { + failure = error; + } else { + throw error; + } + } + + expect(failure?.code).toBe("claude.tool_result_too_large"); + await rejected.translator.failTurn( + rejected.context, + rejected.runId, + failure!.code, + failure!.message, + ); + expect( + rejected.events.some( + (event) => + event.kind === "tool.call.updated" && + payload(event)["toolCallId"] === "tool-oversized" && + payload(event)["status"] === "completed", + ), + ).toBe(false); + expect( + rejected.events.some( + (event) => + event.kind === "tool.call.updated" && + payload(event)["toolCallId"] === "tool-oversized" && + payload(event)["status"] === "failed", + ), + ).toBe(true); + expect( + payload(rejected.events.find((event) => event.kind === "run.failed")!)["error"], + ).toMatchObject({ code: "claude.tool_result_too_large" }); + expect(rejected.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await rejected.logger.destroy(); + }); + + test("rejects an oversized tool input before it can poison terminal delivery", async () => { + const runId = DRIVER_TEST_IDS.runId; + const claudeBootPayload = { + ...driverBootPayload, + runtime: "claude-agent-sdk", + runtimeTransport: "claude-agent-sdk", + } satisfies DriverBootPayload; + const events: DriverEventInput[] = []; + const sseFrameBytes: number[] = []; + const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); + const logger = createBufferedSinkLogger({ + level: "debug", + service: "claude-agent-sdk-published-durability-test", + sink: async () => {}, + }); + let activeRunId: RunId | null = runId; + let sequence = 0; + const context = createAgentDriverContext({ + eventSink: { + currentRunId: () => activeRunId, + pushEvents: async ({ events: batch }) => { + const envelopes = batch.flatMap((event) => + toDriverEventEnvelopes(claudeBootPayload, event, activeRunId), + ); + for (const envelope of envelopes) { + const records = await store.appendDriverEvent( + DRIVER_TEST_IDS.sessionId, + envelope.event, + ); + events.push(envelope.event); + sseFrameBytes.push(...records.map((record) => encodeCmaSseRecord(record).byteLength)); + } + if (batch.some((event) => event.kind === "run.failed")) { + activeRunId = null; + } + return { + accepted: batch.map((event) => ({ + eventId: event.sourceEventId, + seq: (sequence += 1), + type: event.kind, + })), + }; + }, + }, + logger, + payload: createDriverStartInputFromBootPayload(claudeBootPayload), + permission: { request: async () => "allow_once" }, + }); + const publisher = new DriverEventPublisher("claude-agent-sdk", () => "native-session-1"); + const translator = new ClaudeAgentSdkMessageTranslator({ + publicToolCallId: (nativeToolCallId) => nativeToolCallId, + push: (pushContext, reason, batch) => publisher.push(pushContext, reason, batch), + pushTerminal: (pushContext, reason, closures, terminal) => + publisher.pushTerminal(pushContext, reason, closures, terminal), + recordNativeSessionId: async () => {}, + replaceNativeSessionId: async () => {}, + }); + + let failure: ClaudeDurableEventTooLargeError | null = null; + try { + await translator.handleSdkMessage( + context, + { + message: { + content: [ + { + id: "tool-large-input", + input: { data: "x".repeat(1_100_000) }, + name: "Write", + type: "tool_use", + }, + ], + id: "assistant-large-input", + }, + type: "assistant", + uuid: "wire-large-input", + } as unknown as SDKMessage, + runId, + ); + } catch (error) { + if (error instanceof ClaudeDurableEventTooLargeError) failure = error; + else throw error; + } + + expect(failure?.code).toBe("claude.tool_input_too_large"); + await translator.failTurn(context, runId, failure!.code, failure!.message); + expect(events.some((event) => event.kind === "run.failed")).toBe(true); + expect(events.some((event) => payload(event)["rawInput"] !== undefined)).toBe(false); + expect(sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await logger.destroy(); + }); + + test("closes a retained background task start before terminal failure", async () => { + const runId = DRIVER_TEST_IDS.runId; + const claudeBootPayload = { + ...driverBootPayload, + runtime: "claude-agent-sdk", + runtimeTransport: "claude-agent-sdk", + } satisfies DriverBootPayload; + const events: DriverEventInput[] = []; + const logger = createBufferedSinkLogger({ + level: "debug", + service: "claude-agent-sdk-task-retention-test", + sink: async () => {}, + }); + let activeRunId: RunId | null = runId; + let acceptDelivery = false; + let sequence = 0; + const context = createAgentDriverContext({ + eventSink: { + currentRunId: () => activeRunId, + pushEvents: async ({ events: batch }) => { + if (!acceptDelivery) return { accepted: [] }; + const envelopes = batch.flatMap((event) => + toDriverEventEnvelopes(claudeBootPayload, event, activeRunId), + ); + events.push(...envelopes.map(({ event }) => event)); + if (batch.some((event) => event.kind === "run.failed")) activeRunId = null; + return { + accepted: envelopes.map((envelope) => ({ + eventId: envelope.eventId, + seq: (sequence += 1), + type: envelope.event.kind, + })), + }; + }, + }, + logger, + payload: createDriverStartInputFromBootPayload(claudeBootPayload), + permission: { request: async () => "allow_once" }, + }); + const publisher = new DriverEventPublisher("claude-agent-sdk", () => "native-session-1"); + const translator = new ClaudeAgentSdkMessageTranslator({ + publicToolCallId: (nativeToolCallId) => nativeToolCallId, + push: (pushContext, reason, batch) => publisher.push(pushContext, reason, batch), + pushTerminal: (pushContext, reason, closures, terminal) => + publisher.pushTerminal(pushContext, reason, closures, terminal), + recordNativeSessionId: async () => {}, + replaceNativeSessionId: async () => {}, + }); + + await expect( + translator.handleSdkMessage( + context, + { + session_id: "native-session-1", + subtype: "background_tasks_changed", + tasks: [ + { + description: "Inspect the repository", + task_id: "task-1", + task_type: "local_agent", + }, + ], + type: "system", + uuid: "00000000-0000-0000-0000-000000000001", + } as unknown as SDKMessage, + runId, + ), + ).rejects.toThrow(); + + acceptDelivery = true; + await translator.failTurn(context, runId, "claude.task_delivery_failed", "delivery failed"); + + expect( + events.filter((event) => event.kind === "agent.task.updated").map((event) => payload(event)), + ).toMatchObject([ + { active: true, taskId: "task-1" }, + { active: false, taskId: "task-1" }, + ]); + expect(events.at(-1)?.kind).toBe("run.failed"); + await logger.destroy(); + }); + + test("maps oversized native tool IDs and rejects oversized tool names before durable state", async () => { + const accepted = createCmaHarness(); + const nativeToolCallId = `tool-${"x".repeat(1_100_000)}`; + + await accepted.translator.handleSdkMessage( + accepted.context, + { + message: { + content: [{ id: nativeToolCallId, input: {}, name: "Read", type: "tool_use" }], + id: "assistant-long-tool", + }, + type: "assistant", + uuid: "wire-long-tool", + } as unknown as SDKMessage, + accepted.runId, + ); + await accepted.translator.handleSdkMessage( + accepted.context, + { + decision_reason: "Blocked by policy", + message: "Denied by policy", + subtype: "permission_denied", + tool_name: "Read", + tool_use_id: nativeToolCallId, + type: "system", + uuid: "wire-long-tool-advisory", + } as unknown as SDKMessage, + accepted.runId, + ); + await accepted.translator.handleSdkMessage( + accepted.context, + { + is_error: false, + modelUsage: {}, + permission_denials: [{ tool_input: {}, tool_name: "Read", tool_use_id: nativeToolCallId }], + result: "done", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "wire-long-tool-result", + } as unknown as SDKMessage, + accepted.runId, + ); + + const toolEvents = accepted.events.filter( + (event) => event.kind === "item.started" || event.kind === "tool.call.updated", + ); + const publicIds = toolEvents.flatMap((event) => { + const value = payload(event)[event.kind === "item.started" ? "itemId" : "toolCallId"]; + return typeof value === "string" ? [value] : []; + }); + expect(new Set(publicIds).size).toBe(1); + expect(publicIds[0]).not.toBe(nativeToolCallId); + expect( + accepted.events.some( + (event) => + event.kind === "tool.call.updated" && + payload(event)["decisionReason"] === "Blocked by policy", + ), + ).toBe(true); + expect(JSON.stringify(accepted.events)).not.toContain(nativeToolCallId); + expect(accepted.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await accepted.logger.destroy(); + + const rejected = createCmaHarness(); + const oversizedName = "n".repeat(1_100_000); + let failure: ClaudeDurableEventTooLargeError | null = null; + try { + await rejected.translator.handleSdkMessage( + rejected.context, + { + message: { + content: [{ id: "tool-long-name", input: {}, name: oversizedName, type: "tool_use" }], + id: "assistant-long-name", + }, + type: "assistant", + uuid: "wire-long-name", + } as unknown as SDKMessage, + rejected.runId, + ); + } catch (error) { + if (error instanceof ClaudeDurableEventTooLargeError) failure = error; + else throw error; + } + expect(failure?.code).toBe("claude.tool_start_too_large"); + await rejected.translator.failTurn( + rejected.context, + rejected.runId, + failure!.code, + failure!.message, + ); + expect(rejected.events.some((event) => event.kind === "item.started")).toBe(false); + expect( + payload(rejected.events.find((event) => event.kind === "run.failed")!)["error"], + ).toMatchObject({ code: "claude.tool_start_too_large" }); + expect(JSON.stringify(rejected.events)).not.toContain(oversizedName); + await rejected.logger.destroy(); + }); + + test("rejects oversized file paths before publishing a partial durable batch", async () => { + const harness = createCmaHarness(); + let failure: ClaudeDurableEventTooLargeError | null = null; + try { + await harness.translator.handleSdkMessage( + harness.context, + { + failed: [], + files: [{ filename: "f".repeat(1_100_000) }], + subtype: "files_persisted", + type: "system", + } as unknown as SDKMessage, + harness.runId, + ); + } catch (error) { + if (error instanceof ClaudeDurableEventTooLargeError) failure = error; + else throw error; + } + expect(failure?.code).toBe("claude.files_persisted_too_large"); + await harness.translator.failTurn( + harness.context, + harness.runId, + failure!.code, + failure!.message, + ); + expect(harness.events.some((event) => event.kind === "file.change.updated")).toBe(false); + expect( + payload(harness.events.find((event) => event.kind === "run.failed")!)["error"], + ).toMatchObject({ code: "claude.files_persisted_too_large" }); + expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await harness.logger.destroy(); + }); + + test("bounds mirror errors and commits thought and retraction state only after delivery", async () => { + const mirror = createCmaHarness(); + await mirror.translator.handleSdkMessage( + mirror.context, + { + error: "x".repeat(525_000), + key: { subpath: "events.jsonl" }, + subtype: "mirror_error", + type: "system", + uuid: "mirror-large", + } as unknown as SDKMessage, + mirror.runId, + ); + const diagnostic = mirror.events.find((event) => event.kind === "diagnostic.reported"); + expect(diagnostic?.delivery).toBe("best_effort"); + expect(payload(diagnostic!)).toEqual({ + message: "Claude transcript mirror write failed.", + raw: { errorBytes: 525_000, kind: "claude.mirror_error" }, + severity: "error", + }); + expect(mirror.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + await mirror.logger.destroy(); + + const reasons: string[] = []; + const replayedEvents: DriverEventInput[] = []; + let rejectThought = true; + let rejectToolRetraction = true; + const logger = createBufferedSinkLogger({ + level: "debug", + service: "claude-agent-sdk-replay-test", + sink: async () => {}, + }); + const runId = createDriverId() as RunId; + const context = createAgentDriverContext({ + eventSink: { currentRunId: () => runId, pushEvents: async () => ({ accepted: [] }) }, + logger, + payload: bootPayload, + permission: { request: async () => "allow_once" }, + }); + const translator = new ClaudeAgentSdkMessageTranslator({ + publicToolCallId: (nativeToolCallId) => nativeToolCallId, + push: async (_context, reason, events) => { + reasons.push(reason); + if (reason === "driver.claude.thought.completed" && rejectThought) { + rejectThought = false; + throw new Error("thought delivery failed"); + } + if (reason === "driver.claude.tool.retracted" && rejectToolRetraction) { + rejectToolRetraction = false; + throw new Error("tool retraction failed"); + } + replayedEvents.push(...events); + }, + pushTerminal: async () => {}, + recordNativeSessionId: async () => {}, + replaceNativeSessionId: async () => {}, + }); + + await translator.handleSdkMessage( + context, + { + event: { + content_block: { thinking: "", type: "thinking" }, + index: 0, + type: "content_block_start", + }, + type: "stream_event", + uuid: "thought-wire", + } as unknown as SDKMessage, + runId, + ); + await expect(translator.endActiveThought(context)).rejects.toThrow("thought delivery failed"); + await expect(translator.endActiveThought(context)).resolves.toBeUndefined(); + expect(reasons.filter((reason) => reason === "driver.claude.thought.completed")).toHaveLength( + 2, + ); + + await translator.handleSdkMessage( + context, + { + message: { + content: [ + { text: "stale", type: "text" }, + { id: "tool-stale", input: {}, name: "Read", type: "tool_use" }, + ], + id: "assistant-stale", + }, + type: "assistant", + uuid: "wire-stale", + } as unknown as SDKMessage, + runId, + ); + const fallback = { + retracted_message_uuids: ["wire-stale"], + subtype: "model_refusal_fallback", + type: "system", + uuid: "fallback", + } as unknown as SDKMessage; + await expect(translator.handleSdkMessage(context, fallback, runId)).rejects.toThrow( + "tool retraction failed", + ); + await expect(translator.handleSdkMessage(context, fallback, runId)).resolves.toBe(false); + expect( + replayedEvents.filter( + (event) => event.kind === "message.cancelled" && payload(event)["reason"] === "superseded", + ), + ).toHaveLength(1); + expect( + replayedEvents.filter( + (event) => + event.kind === "tool.call.updated" && + payload(event)["toolCallId"] === "tool-stale" && + payload(event)["status"] === "cancelled", + ), + ).toHaveLength(1); + await logger.destroy(); + }); +}); diff --git a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts index ae1bb2e..3b71422 100644 --- a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts @@ -10,6 +10,7 @@ import type { RunId } from "../src/protocol/id"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { ClaudeAgentSdkMessageTranslator } from "../src/runtimes/claude/agent-sdk-message-translator"; +import { isRecord, messageText } from "./claude-agent-sdk-test-helpers"; import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; interface EventBatch { @@ -29,13 +30,9 @@ const claudeFixtureNames = [ "result-failure-diagnostic", "stream-text-thinking-tool-result", "system-files-and-session", - "unknown-message-ignored", + "unknown-message-diagnostic", ] as const; -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - function readJsonFixture(path: string): unknown { return JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")); } @@ -159,6 +156,7 @@ function createHarness() { }); const context: AgentDriverContext = createAgentDriverContext({ eventSink: { + currentRunId: () => "run-1" as RunId, pushEvents: async () => ({ accepted: [] }), }, logger, @@ -168,12 +166,17 @@ function createHarness() { }, }); const translator = new ClaudeAgentSdkMessageTranslator({ + publicToolCallId: (nativeToolCallId) => nativeToolCallId, push: async (_context, reason, events) => { batches.push({ events, reason }); }, + pushTerminal: async (_context, reason, closures, terminal) => { + batches.push({ events: [...closures, terminal], reason }); + }, recordNativeSessionId: async (_context, sessionId) => { nativeSessionIds.push(sessionId); }, + replaceNativeSessionId: async () => {}, }); return { @@ -283,7 +286,8 @@ describe("Claude Agent SDK provider fixtures", () => { expect(textMessages.map((entry) => entry.contentDelta)).toEqual(["Got it —", " I'm here."]); expect(new Set(textMessages.map((entry) => entry.messageId)).size).toBe(1); expect(payload?.["finalMessageId"]).toBe(textMessages[0]?.messageId); - expect(payload?.["finalMessageText"]).toBe("Got it — I'm here."); + expect(payload).not.toHaveProperty("finalMessageText"); + expect(messageText(events(), payload?.["finalMessageId"])).toBe("Got it — I'm here."); }); test("scopes interleaved subagent stream chunks away from the main message", async () => { @@ -455,7 +459,366 @@ describe("Claude Agent SDK provider fixtures", () => { { text: "recovered final answer", type: "text" }, ]); expect(completedPayload?.["finalMessageId"]).toBe(snapshotPayload?.["messageId"]); - expect(completedPayload?.["finalMessageText"]).toBe("recovered final answer"); + expect(completedPayload).not.toHaveProperty("finalMessageText"); + expect(messageText(translated, completedPayload?.["finalMessageId"])).toBe( + "recovered final answer", + ); + }); + + test("keeps structured output as the canonical final payload", async () => { + const { context, events, logger, translator } = createHarness(); + + await translator.handleSdkMessage( + context, + { + is_error: false, + modelUsage: {}, + permission_denials: [], + result: "structured output placeholder", + structured_output: { answer: 42, citations: ["source-1"] }, + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-structured", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + await logger.destroy(); + + expect(events().map(({ kind }) => kind)).not.toContain("message.added"); + expect(events()).toContainEqual({ + kind: "run.completed", + payload: { + stopReason: "end_turn", + structuredOutput: { answer: 42, citations: ["source-1"] }, + }, + runId: "run-1" as RunId, + }); + }); + + test("fails closed for a non-JSON structured output", async () => { + const { context, events, logger, translator } = createHarness(); + + await translator.handleSdkMessage( + context, + { + is_error: false, + modelUsage: {}, + permission_denials: [], + result: "structured output placeholder", + structured_output: Symbol("invalid"), + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-invalid-structured", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + await logger.destroy(); + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "run.failed", + payload: expect.objectContaining({ + error: expect.objectContaining({ code: "claude.invalid_structured_output" }), + }), + }), + ); + expect(events().map(({ kind }) => kind)).not.toContain("run.completed"); + }); + + test("retracts a superseded refusal before publishing its replacement", async () => { + const { context, events, logger, translator } = createHarness(); + const messages = [ + { + message: { + content: [{ text: "stale refusal", type: "text" }], + id: "native-refusal", + }, + parent_tool_use_id: null, + session_id: "session-1", + type: "assistant", + uuid: "wire-refusal", + }, + { + message: { + content: [{ text: "canonical replacement", type: "text" }], + id: "native-replacement", + }, + parent_tool_use_id: null, + session_id: "session-1", + supersedes: ["wire-refusal"], + type: "assistant", + uuid: "wire-replacement", + }, + { + is_error: false, + modelUsage: {}, + permission_denials: [], + result: "canonical replacement", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-replacement", + }, + ] as unknown as SDKMessage[]; + + for (const message of messages) { + await translator.handleSdkMessage(context, message, "run-1" as RunId); + } + await logger.destroy(); + + const snapshots = events().flatMap((event) => { + if (event.kind !== "message.added" || !isRecord(event.payload)) { + return []; + } + const content = event.payload["content"]; + const messageId = event.payload["messageId"]; + return Array.isArray(content) && typeof messageId === "string" + ? [{ messageId, text: (content[0] as { text?: unknown } | undefined)?.text }] + : []; + }); + const stale = snapshots.find(({ text }) => text === "stale refusal"); + const replacement = snapshots.find(({ text }) => text === "canonical replacement"); + const cancellationIndex = events().findIndex( + (event) => + event.kind === "message.cancelled" && + isRecord(event.payload) && + event.payload["messageId"] === stale?.messageId, + ); + const replacementIndex = events().findIndex( + (event) => + event.kind === "message.added" && + isRecord(event.payload) && + event.payload["messageId"] === replacement?.messageId, + ); + const completed = events().find((event) => event.kind === "run.completed"); + + expect(stale).toBeDefined(); + expect(replacement).toBeDefined(); + expect(cancellationIndex).toBeGreaterThan(-1); + expect(cancellationIndex).toBeLessThan(replacementIndex); + expect(completed?.payload).toMatchObject({ + finalMessageId: replacement?.messageId, + }); + expect(completed?.payload).not.toHaveProperty("finalMessageText"); + expect(messageText(events(), replacement?.messageId)).toBe("canonical replacement"); + }); + + test("applies refusal fallback retractions idempotently to messages and tool results", async () => { + const { context, events, logger, translator } = createHarness(); + const fallback = { + content: "Retrying with fallback model.", + direction: "retry", + fallback_model: "claude-fallback", + original_model: "claude-primary", + request_id: "request-1", + retracted_message_uuids: ["wire-refusal", "wire-tool-result", "unknown-wire"], + session_id: "session-1", + subtype: "model_refusal_fallback", + trigger: "refusal", + type: "system", + uuid: "fallback-notice", + }; + const messages = [ + { + message: { + content: [ + { text: "stale refusal", type: "text" }, + { id: "tool-old", input: { command: "pwd" }, name: "Bash", type: "tool_use" }, + ], + id: "native-refusal", + }, + parent_tool_use_id: null, + session_id: "session-1", + type: "assistant", + uuid: "wire-refusal", + }, + { + message: { + content: [{ content: "stale tool result", tool_use_id: "tool-old", type: "tool_result" }], + }, + session_id: "session-1", + type: "user", + uuid: "wire-tool-result", + }, + fallback, + { ...fallback, uuid: "fallback-notice-replayed" }, + { + is_error: false, + modelUsage: {}, + permission_denials: [ + { + decisionReason: "denied before fallback", + tool_input: { command: "pwd" }, + tool_name: "Bash", + tool_use_id: "tool-old", + }, + ], + result: "canonical replacement", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "late-authoritative-denial", + }, + { + message: { + content: [{ text: "canonical replacement", type: "text" }], + id: "native-replacement", + }, + parent_tool_use_id: null, + session_id: "session-1", + type: "assistant", + uuid: "wire-replacement", + }, + { + is_error: false, + modelUsage: {}, + permission_denials: [], + result: "canonical replacement", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-replacement", + }, + ] as unknown as SDKMessage[]; + + for (const message of messages) { + await translator.handleSdkMessage(context, message, "run-1" as RunId); + } + await logger.destroy(); + + const retractedMessages = events().filter( + (event) => + event.kind === "message.cancelled" && + isRecord(event.payload) && + event.payload["reason"] === "superseded", + ); + const cancelledTools = events().filter( + (event) => + event.kind === "tool.call.updated" && + isRecord(event.payload) && + event.payload["status"] === "cancelled" && + event.payload["toolCallId"] === "tool-old", + ); + const completed = events().find((event) => event.kind === "run.completed"); + const retractionIndex = events().findIndex( + (event) => + event.kind === "tool.call.updated" && + isRecord(event.payload) && + event.payload["status"] === "cancelled" && + event.payload["toolCallId"] === "tool-old", + ); + const toolUpdatesAfterRetraction = events() + .slice(retractionIndex + 1) + .filter( + (event) => + event.kind === "tool.call.updated" && + isRecord(event.payload) && + event.payload["toolCallId"] === "tool-old", + ); + + expect(retractedMessages).toHaveLength(1); + expect(cancelledTools).toHaveLength(1); + expect(toolUpdatesAfterRetraction).toEqual([]); + expect(completed?.payload).not.toHaveProperty("finalMessageText"); + expect( + messageText( + events(), + isRecord(completed?.payload) ? completed.payload["finalMessageId"] : null, + ), + ).toBe("canonical replacement"); + }); + + test("keeps assistant envelopes distinct when they share an API message id", async () => { + const { context, events, logger, translator } = createHarness(); + const messages = [ + { + message: { content: [{ text: "first", type: "text" }], id: "shared-native" }, + parent_tool_use_id: null, + session_id: "session-1", + type: "assistant", + uuid: "wire-first", + }, + { + message: { content: [{ text: "second", type: "text" }], id: "shared-native" }, + parent_tool_use_id: null, + session_id: "session-1", + type: "assistant", + uuid: "wire-second", + }, + { + is_error: false, + modelUsage: {}, + permission_denials: [], + result: "second", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-second", + }, + ] as unknown as SDKMessage[]; + + for (const message of messages) { + await translator.handleSdkMessage(context, message, "run-1" as RunId); + } + await logger.destroy(); + + const snapshots = events().flatMap((event) => { + if (event.kind !== "message.added" || !isRecord(event.payload)) { + return []; + } + const content = event.payload["content"]; + const messageId = event.payload["messageId"]; + return Array.isArray(content) && typeof messageId === "string" + ? [{ messageId, text: (content[0] as { text?: unknown } | undefined)?.text }] + : []; + }); + const completed = events().find((event) => event.kind === "run.completed"); + + expect(snapshots.map(({ text }) => text)).toEqual(["first", "second"]); + expect(new Set(snapshots.map(({ messageId }) => messageId)).size).toBe(2); + expect(completed?.payload).toMatchObject({ + finalMessageId: snapshots[1]?.messageId, + }); + expect(completed?.payload).not.toHaveProperty("finalMessageText"); + expect(messageText(events(), snapshots[1]?.messageId)).toBe("second"); + }); + + test("turns provider-aborted result frames into runtime cancellation", async () => { + const { context, events, logger, translator } = createHarness(); + + await translator.handleSdkMessage( + context, + { + errors: ["aborted"], + is_error: true, + modelUsage: {}, + permission_denials: [], + subtype: "error_during_execution", + terminal_reason: "aborted_tools", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-aborted", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + await logger.destroy(); + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "run.cancelled", + payload: expect.objectContaining({ reason: "aborted_tools" }), + }), + ); + expect(events().map(({ kind }) => kind)).not.toContain("run.failed"); }); test("does not let a late older assistant completion replace the final message", async () => { @@ -504,7 +867,8 @@ describe("Claude Agent SDK provider fixtures", () => { expect(runCompleted).toBeDefined(); expect(payload).not.toBeNull(); - expect(payload?.["finalMessageText"]).toBe("最终回答 B"); + expect(payload).not.toHaveProperty("finalMessageText"); + expect(messageText(events(), payload?.["finalMessageId"])).toBe("最终回答 B"); }); test("fails closed when a newer assistant is still incomplete at result success", async () => { diff --git a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts index 735a852..8391032 100644 --- a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts @@ -8,6 +8,7 @@ import type { RunId } from "../src/protocol/id"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { ClaudeAgentSdkMessageTranslator } from "../src/runtimes/claude/agent-sdk-message-translator"; +import { isRecord, messageText } from "./claude-agent-sdk-test-helpers"; import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; interface EventBatch { @@ -15,13 +16,10 @@ interface EventBatch { readonly reason: string; } -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function createHarness() { +function createHarness(publicToolCallId: (nativeToolCallId: string) => string = (id) => id) { const batches: EventBatch[] = []; const nativeSessionIds: string[] = []; + const nativeSessionResets: Array = []; const logger = createBufferedSinkLogger({ level: "debug", service: "claude-agent-sdk-provider-fixtures-test", @@ -29,6 +27,7 @@ function createHarness() { }); const context: AgentDriverContext = createAgentDriverContext({ eventSink: { + currentRunId: () => "run-1" as RunId, pushEvents: async () => ({ accepted: [] }), }, logger, @@ -38,12 +37,19 @@ function createHarness() { }, }); const translator = new ClaudeAgentSdkMessageTranslator({ + publicToolCallId, push: async (_context, reason, events) => { batches.push({ events, reason }); }, + pushTerminal: async (_context, reason, closures, terminal) => { + batches.push({ events: [...closures, terminal], reason }); + }, recordNativeSessionId: async (_context, sessionId) => { nativeSessionIds.push(sessionId); }, + replaceNativeSessionId: async (_context, previousSessionId, nextSessionId) => { + nativeSessionResets.push([previousSessionId, nextSessionId]); + }, }); return { @@ -51,11 +57,370 @@ function createHarness() { events: () => batches.flatMap((batch) => batch.events), logger, nativeSessionIds, + nativeSessionResets, translator, }; } describe("Claude Agent SDK provider fixtures", () => { + test("projects visible task activity without leaking task internals", async () => { + const { context, events, logger, translator } = createHarness(); + const messages = [ + { + description: "Inspect the repository", + is_backgrounded: true, + prompt: "private task prompt", + session_id: "native-session-1", + subtype: "task_started", + task_id: "task-1", + task_type: "local_agent", + tool_use_id: "tool-1", + type: "system", + uuid: "task-started-1", + }, + { + description: "Inspecting tests", + last_tool_name: "Read", + session_id: "native-session-1", + subtype: "task_progress", + summary: "private progress summary", + task_id: "task-1", + tool_use_id: "tool-1", + type: "system", + usage: { duration_ms: 10, tool_uses: 1, total_tokens: 50 }, + uuid: "task-progress-1", + }, + { + patch: { description: "Finalizing", error: "private task error", status: "running" }, + session_id: "native-session-1", + subtype: "task_updated", + task_id: "task-1", + type: "system", + uuid: "task-updated-1", + }, + { + output_file: "/tmp/private-task-output", + session_id: "native-session-1", + status: "completed", + subtype: "task_notification", + summary: "private terminal summary", + task_id: "task-1", + tool_use_id: "tool-1", + type: "system", + usage: { duration_ms: 25, tool_uses: 2, total_tokens: 100 }, + uuid: "task-notification-1", + }, + { + session_id: "native-session-1", + subtype: "background_tasks_changed", + tasks: [ + { + description: "Inspect the repository", + task_id: "task-1", + task_type: "local_agent", + }, + { + ambient: true, + description: "private ambient task", + task_id: "ambient-task", + task_type: "local_agent", + }, + ], + type: "system", + uuid: "background-tasks-1", + }, + { + session_id: "native-session-1", + subtype: "background_tasks_changed", + tasks: [], + type: "system", + uuid: "background-tasks-2", + }, + ] as unknown as SDKMessage[]; + + for (const message of messages) { + await translator.handleSdkMessage(context, message, "run-1" as RunId); + } + await logger.destroy(); + + const taskEvents = events().filter((event) => event.kind === "agent.task.updated"); + expect(taskEvents).toHaveLength(2); + expect(taskEvents.at(0)).toMatchObject({ + delivery: "lossless", + payload: { + active: true, + taskId: "task-1", + taskType: "local_agent", + title: "Inspect the repository", + }, + }); + expect(taskEvents.at(1)).toEqual({ + delivery: "lossless", + kind: "agent.task.updated", + payload: { active: false, taskId: "task-1" }, + }); + expect(JSON.stringify(taskEvents)).not.toContain("private"); + expect(events().filter((event) => event.kind === "diagnostic.reported")).toHaveLength(4); + }); + + test("closes visible tasks before the run terminal and resets them between turns", async () => { + const { context, events, logger, translator } = createHarness(); + + await translator.handleSdkMessage( + context, + { + session_id: "native-session-1", + subtype: "background_tasks_changed", + tasks: [ + { + description: "Inspect the repository", + task_id: "task-1", + task_type: "local_agent", + }, + ], + type: "system", + uuid: "background-tasks-1", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + await translator.handleSdkMessage( + context, + { + result: "", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-1", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + + const terminalIndex = events().findIndex((event) => event.kind === "run.completed"); + expect(terminalIndex).toBeGreaterThan(0); + expect(events().slice(0, terminalIndex)).toContainEqual({ + delivery: "lossless", + kind: "agent.task.updated", + payload: { active: false, taskId: "task-1" }, + }); + + const beforeReset = events().length; + translator.resetTurnMessageState(); + await translator.handleSdkMessage( + context, + { + session_id: "native-session-1", + subtype: "background_tasks_changed", + tasks: [ + { + description: "Inspect the repository again", + task_id: "task-1", + task_type: "local_agent", + }, + ], + type: "system", + uuid: "background-tasks-2", + } as unknown as SDKMessage, + "run-2" as RunId, + ); + expect(events().slice(beforeReset)).toContainEqual({ + delivery: "lossless", + kind: "agent.task.updated", + payload: { + active: true, + taskId: "task-1", + taskType: "local_agent", + title: "Inspect the repository again", + }, + }); + await logger.destroy(); + }); + + test("projects informational, local command, mirror failure, and conversation reset frames", async () => { + const { context, events, logger, nativeSessionResets, translator } = createHarness(); + const messages = [ + { + content: "A stop hook blocked continuation.", + level: "warning", + prevent_continuation: true, + session_id: "native-session-1", + subtype: "informational", + tool_use_id: "tool-1", + type: "system", + uuid: "informational-1", + }, + { + error: "Transcript mirror write failed.", + key: { + projectKey: "project-1", + sessionId: "native-session-1", + subpath: "events.jsonl", + }, + session_id: "native-session-1", + subtype: "mirror_error", + type: "system", + uuid: "mirror-1", + }, + { + new_conversation_id: "native-session-2", + session_id: "native-session-1", + type: "conversation_reset", + uuid: "reset-1", + }, + { + content: "Local command output.", + session_id: "native-session-2", + subtype: "local_command_output", + type: "system", + uuid: "local-command-1", + }, + ] as unknown as SDKMessage[]; + + for (const message of messages) { + await translator.handleSdkMessage(context, message, "run-1" as RunId); + } + await logger.destroy(); + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "message.added", + payload: expect.objectContaining({ + content: [{ text: "A stop hook blocked continuation.", type: "text" }], + level: "warning", + preventContinuation: true, + subtype: "informational", + toolCallId: "tool-1", + }), + }), + ); + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "message.added", + payload: expect.objectContaining({ + content: [{ text: "Local command output.", type: "text" }], + subtype: "local_command_output", + }), + }), + ); + expect(events()).toContainEqual( + expect.objectContaining({ + delivery: "best_effort", + kind: "diagnostic.reported", + payload: expect.objectContaining({ + message: "Claude transcript mirror write failed.", + raw: { + errorBytes: 31, + kind: "claude.mirror_error", + }, + severity: "error", + }), + }), + ); + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "session.info.updated", + payload: expect.objectContaining({ title: null }), + }), + ); + expect(nativeSessionResets).toEqual([["native-session-1", "native-session-2"]]); + }); + + test("cancels a truncated assistant frame", async () => { + const { context, events, logger, translator } = createHarness(); + + await translator.handleSdkMessage( + context, + { + aborted: true, + message: { content: [{ text: "partial", type: "text" }] }, + type: "assistant", + uuid: "assistant-aborted", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + await logger.destroy(); + + const transcript = events(); + expect(transcript).toContainEqual( + expect.objectContaining({ + kind: "message.added", + payload: expect.objectContaining({ content: [{ text: "partial", type: "text" }] }), + }), + ); + expect(transcript.findIndex(({ kind }) => kind === "message.added")).toBeLessThan( + transcript.findIndex(({ kind }) => kind === "message.cancelled"), + ); + expect(transcript.map(({ kind }) => kind)).not.toContain("message.completed"); + }); + + test("fails an assistant frame carrying an SDK error", async () => { + const { context, events, logger, translator } = createHarness(); + + await translator.handleSdkMessage( + context, + { + error: "rate_limit", + message: { content: [] }, + type: "assistant", + uuid: "assistant-error", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + await logger.destroy(); + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "message.failed", + payload: expect.objectContaining({ + error: expect.objectContaining({ code: "claude.rate_limit", retryable: true }), + }), + }), + ); + expect(events().findIndex(({ kind }) => kind === "message.started")).toBeLessThan( + events().findIndex(({ kind }) => kind === "message.failed"), + ); + expect(events().map(({ kind }) => kind)).not.toContain("message.completed"); + }); + + test("fails a terminating API error carried by a success result", async () => { + const { context, events, logger, translator } = createHarness(); + + await translator.handleSdkMessage( + context, + { + api_error_status: 529, + is_error: true, + modelUsage: {}, + permission_denials: [], + result: "API Error: 529", + session_id: "native-session-1", + stop_reason: null, + subtype: "success", + terminal_reason: "api_error", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-api-error", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + await logger.destroy(); + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "run.failed", + payload: expect.objectContaining({ + error: expect.objectContaining({ + details: { apiErrorStatus: 529, terminalReason: "api_error" }, + retryable: true, + }), + recoverable: true, + }), + }), + ); + expect(events().map(({ kind }) => kind)).not.toContain("run.completed"); + }); + test("marks an SDK tool error as failed", async () => { const { context, events, logger, translator } = createHarness(); const messages = [ @@ -95,6 +460,189 @@ describe("Claude Agent SDK provider fixtures", () => { ); }); + test("classifies wrapper-level tool non-execution metadata", async () => { + const { context, events, logger, translator } = createHarness(); + const messages = [ + { + message: { + content: [{ id: "tool-1", input: {}, name: "Bash", type: "tool_use" }], + }, + type: "assistant", + uuid: "assistant-1", + }, + { + message: { + content: [ + { + content: "Request interrupted", + is_error: true, + tool_use_id: "tool-1", + type: "tool_result", + }, + ], + }, + tool_result_meta: [ + { + id: "tool-1", + non_execution_kind: "interrupted", + user_feedback: "Stop here", + }, + ], + tool_use_result: { + usage: { output_tokens_details: { thinking_tokens: 3 } }, + }, + type: "user", + uuid: "user-1", + }, + ] as unknown as SDKMessage[]; + + for (const message of messages) { + await translator.handleSdkMessage(context, message, "run-1" as RunId); + } + await logger.destroy(); + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "tool.call.updated", + payload: expect.objectContaining({ + nonExecutionKind: "interrupted", + status: "cancelled", + structuredOutput: { + usage: { output_tokens_details: { thinking_tokens: 3 } }, + }, + toolCallId: "tool-1", + userFeedback: "Stop here", + }), + }), + ); + }); + + test("materializes authoritative result permission denials without a tool result", async () => { + const { context, events, logger, translator } = createHarness(); + + await translator.handleSdkMessage( + context, + { + decision_reason: "Blocked by policy X", + decision_reason_type: "rule", + message: "Denied by policy X", + subtype: "permission_denied", + tool_name: "Bash", + tool_use_id: "tool-denied", + type: "system", + uuid: "denial-advisory", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + await translator.handleSdkMessage( + context, + { + is_error: false, + modelUsage: {}, + permission_denials: [ + { tool_input: { command: "pwd" }, tool_name: "Bash", tool_use_id: "tool-denied" }, + ], + result: "done", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-1", + } as unknown as SDKMessage, + "run-1" as RunId, + ); + await logger.destroy(); + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "tool.call.updated", + payload: expect.objectContaining({ + rawInput: '{"command":"pwd"}', + content: "Denied by policy X", + decisionReason: "Blocked by policy X", + decisionReasonType: "rule", + status: "failed", + title: "Bash", + toolCallId: "tool-denied", + }), + }), + ); + expect(events().filter(({ kind }) => kind === "item.started")).toHaveLength(1); + expect(events().filter(({ kind }) => kind === "item.completed")).toHaveLength(1); + }); + + test("lets result permission denials override earlier tool terminals", async () => { + const { context, events, logger, translator } = createHarness(); + const messages = [ + { + message: { + content: [ + { id: "tool-completed", input: {}, name: "Read", type: "tool_use" }, + { id: "tool-cancelled", input: {}, name: "Bash", type: "tool_use" }, + ], + }, + type: "assistant", + uuid: "assistant-1", + }, + { + message: { + content: [ + { content: "ok", tool_use_id: "tool-completed", type: "tool_result" }, + { + content: "interrupted", + is_error: true, + tool_use_id: "tool-cancelled", + type: "tool_result", + }, + ], + }, + tool_result_meta: [{ id: "tool-cancelled", non_execution_kind: "cancelled" }], + type: "user", + uuid: "user-1", + }, + { + is_error: false, + modelUsage: {}, + permission_denials: [ + { tool_input: {}, tool_name: "Read", tool_use_id: "tool-completed" }, + { tool_input: {}, tool_name: "Bash", tool_use_id: "tool-cancelled" }, + ], + result: "done", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-1", + }, + ] as unknown as SDKMessage[]; + + for (const message of messages) { + await translator.handleSdkMessage(context, message, "run-1" as RunId); + } + await logger.destroy(); + + for (const toolCallId of ["tool-completed", "tool-cancelled"]) { + const statuses = events().flatMap((event) => { + if (event.kind !== "tool.call.updated" || !isRecord(event.payload)) { + return []; + } + return event.payload["toolCallId"] === toolCallId && + typeof event.payload["status"] === "string" + ? [event.payload["status"]] + : []; + }); + expect(statuses.at(-1)).toBe("failed"); + expect( + events().filter( + (event) => + event.kind === "item.completed" && + isRecord(event.payload) && + event.payload["itemId"] === toolCallId, + ), + ).toHaveLength(1); + } + }); + test("rotates assistant identity across a tool boundary and marks the final message", async () => { const { context, events, logger, translator } = createHarness(); const messages = [ @@ -223,7 +771,8 @@ describe("Claude Agent SDK provider fixtures", () => { const completedIndex = translated.findIndex((event) => event.kind === "message.completed"); const terminalIndex = translated.findIndex((event) => event.kind === "run.completed"); - expect(payload?.["finalMessageText"]).toBe("完整最终回答"); + expect(payload).not.toHaveProperty("finalMessageText"); + expect(messageText(events(), payload?.["finalMessageId"])).toBe("完整最终回答"); expect(snapshot).toMatchObject({ kind: "message.added", payload: { @@ -403,9 +952,14 @@ describe("Claude Agent SDK provider fixtures", () => { event.payload["messageId"] === messageId, ), ).toEqual([]); - expect(translated.find((event) => event.kind === "run.completed")).toMatchObject({ - payload: expect.objectContaining({ finalMessageText: "complete" }), - }); + const terminal = translated.find((event) => event.kind === "run.completed"); + expect(terminal?.payload).not.toHaveProperty("finalMessageText"); + expect( + messageText( + translated, + isRecord(terminal?.payload) ? terminal.payload["finalMessageId"] : null, + ), + ).toBe("complete"); }); test("repairs streamed tool input with a lossless assistant snapshot", async () => { @@ -519,7 +1073,11 @@ describe("Claude Agent SDK provider fixtures", () => { ["run.completed", "run.failed"].includes(event.kind), ); expect(terminalIndex).toBeGreaterThan(-1); - for (const kind of ["message.completed", "thought.completed", "item.completed"] as const) { + const closureKinds = + outcome === "success" + ? (["message.completed", "thought.completed", "item.completed"] as const) + : (["message.failed", "thought.cancelled", "item.completed"] as const); + for (const kind of closureKinds) { expect(translated.findIndex((event) => event.kind === kind)).toBeGreaterThan(-1); expect(translated.findIndex((event) => event.kind === kind)).toBeLessThan(terminalIndex); } @@ -628,7 +1186,10 @@ describe("Claude Agent SDK provider fixtures", () => { { text: "Pong. What would you like to work on?", type: "text" }, ]); expect(payload?.["finalMessageId"]).toBe(textMessages[0]?.messageId); - expect(payload?.["finalMessageText"]).toBe("Pong. What would you like to work on?"); + expect(payload).not.toHaveProperty("finalMessageText"); + expect(messageText(translated, payload?.["finalMessageId"])).toBe( + "Pong. What would you like to work on?", + ); }); test("anchors uuid-fractured stream fragments to one closed assistant message", async () => { @@ -752,7 +1313,141 @@ describe("Claude Agent SDK provider fixtures", () => { expect(textMessages.map((entry) => entry.contentDelta)).toEqual(["相同文本"]); expect(snapshotPayload?.["messageId"]).toBe(textMessages[0]?.messageId); expect(payload?.["finalMessageId"]).toBe(textMessages[0]?.messageId); - expect(payload?.["finalMessageText"]).toBe("相同文本"); + expect(payload).not.toHaveProperty("finalMessageText"); + expect(messageText(events(), payload?.["finalMessageId"])).toBe("相同文本"); + }); + + test("keeps a confirmed streamed assistant replay on its original message", async () => { + const { context, events, logger, translator } = createHarness(); + const assistant = { + message: { + content: [{ text: "canonical", type: "text" }], + id: "native-message", + }, + type: "assistant", + uuid: "wire-assistant", + }; + const messages = [ + { + event: { + message: { id: "native-message" }, + type: "message_start", + }, + type: "stream_event", + uuid: "stream-start", + }, + { + event: { + delta: { text: "canonical", type: "text_delta" }, + type: "content_block_delta", + }, + type: "stream_event", + uuid: "stream-delta", + }, + { + event: { type: "message_stop" }, + type: "stream_event", + uuid: "stream-stop", + }, + assistant, + assistant, + { + result: "canonical", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-1", + }, + ] as unknown as SDKMessage[]; + + for (const message of messages) { + await translator.handleSdkMessage(context, message, "run-1" as RunId); + } + await logger.destroy(); + + const started = events().filter((event) => event.kind === "message.started"); + const completed = events().filter((event) => event.kind === "message.completed"); + const snapshots = events().filter((event) => event.kind === "message.added"); + const runCompleted = events().find((event) => event.kind === "run.completed"); + const startedMessageId = isRecord(started[0]?.payload) + ? started[0].payload["messageId"] + : undefined; + + expect(started).toHaveLength(1); + expect(completed).toHaveLength(1); + expect(snapshots).toHaveLength(1); + expect(runCompleted?.payload).toMatchObject({ + finalMessageId: startedMessageId, + }); + expect(runCompleted?.payload).not.toHaveProperty("finalMessageText"); + expect(messageText(events(), startedMessageId)).toBe("canonical"); + }); + + test("keeps distinct live assistant envelopes that share a native message id", async () => { + const { context, events, logger, translator } = createHarness(); + const messages = [ + { + event: { message: { id: "shared-native" }, type: "message_start" }, + type: "stream_event", + uuid: "stream-start", + }, + { + event: { + delta: { text: "first", type: "text_delta" }, + type: "content_block_delta", + }, + type: "stream_event", + uuid: "stream-first", + }, + { + message: { content: [{ text: "first", type: "text" }], id: "shared-native" }, + type: "assistant", + uuid: "wire-first", + }, + { + event: { + delta: { text: "second", type: "text_delta" }, + type: "content_block_delta", + }, + type: "stream_event", + uuid: "stream-second", + }, + { + message: { content: [{ text: "second", type: "text" }], id: "shared-native" }, + type: "assistant", + uuid: "wire-second", + }, + { + result: "second", + subtype: "success", + total_cost_usd: 0, + type: "result", + usage: {}, + uuid: "result-1", + }, + ] as unknown as SDKMessage[]; + + for (const message of messages) { + await translator.handleSdkMessage(context, message, "run-1" as RunId); + } + await logger.destroy(); + + const snapshots = events().filter((event) => event.kind === "message.added"); + const messageIds = snapshots.flatMap((event) => + isRecord(event.payload) && typeof event.payload["messageId"] === "string" + ? [event.payload["messageId"]] + : [], + ); + const runCompleted = events().find((event) => event.kind === "run.completed"); + + expect(snapshots).toHaveLength(2); + expect(new Set(messageIds).size).toBe(2); + expect(runCompleted?.payload).toMatchObject({ + finalMessageId: messageIds.at(-1), + }); + expect(runCompleted?.payload).not.toHaveProperty("finalMessageText"); + expect(messageText(events(), messageIds.at(-1))).toBe("second"); }); test("keeps duplicate text on distinct messages when the stream identity is confirmed", async () => { @@ -823,6 +1518,7 @@ describe("Claude Agent SDK provider fixtures", () => { expect(textMessages.map((entry) => entry.contentDelta)).toEqual(["相同文本", "相同文本"]); expect(new Set(textMessages.map((entry) => entry.messageId)).size).toBe(2); expect(payload?.["finalMessageId"]).toBe(textMessages.at(-1)?.messageId); - expect(payload?.["finalMessageText"]).toBe("相同文本"); + expect(payload).not.toHaveProperty("finalMessageText"); + expect(messageText(events(), payload?.["finalMessageId"])).toBe("相同文本"); }); }); diff --git a/tests/claude-agent-sdk-query-options.test.ts b/tests/claude-agent-sdk-query-options.test.ts index 2c878fa..49ab534 100644 --- a/tests/claude-agent-sdk-query-options.test.ts +++ b/tests/claude-agent-sdk-query-options.test.ts @@ -184,6 +184,7 @@ describe("Claude Agent SDK query options", () => { let permissionSignal: AbortSignal | undefined; const context = createAgentDriverContext({ eventSink: { + currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, logger, @@ -233,6 +234,15 @@ describe("Claude Agent SDK query options", () => { "Bash", { command: "pwd" }, { + agentID: "subagent-1", + blockedPath: "/workspace/secret", + decisionReason: "Path is outside the allowed roots.", + description: "Read access to /workspace/secret", + matchedAskRule: { + ruleContent: "Bash(*)", + source: "project", + toolName: "Bash", + }, requestId: "permission-request-1", signal: abortController.signal, toolUseID: "tool-1", @@ -252,6 +262,15 @@ describe("Claude Agent SDK query options", () => { await drainClaudeTasks(permissionTasks); expect(permissionTasks.size).toBe(0); expect(permissionInput).toMatchObject({ + agentId: "subagent-1", + blockedPath: "/workspace/secret", + decisionReason: "Path is outside the allowed roots.", + description: "Read access to /workspace/secret", + matchedAskRule: { + ruleContent: "Bash(*)", + source: "project", + toolName: "Bash", + }, requestId: "permission-request-1", toolCallId: "tool-1", }); @@ -282,6 +301,7 @@ describe("Claude Agent SDK query options", () => { const logger = createTestLogger(); const context = createAgentDriverContext({ eventSink: { + currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, logger, diff --git a/tests/claude-agent-sdk-task-events.test.ts b/tests/claude-agent-sdk-task-events.test.ts new file mode 100644 index 0000000..a43d126 --- /dev/null +++ b/tests/claude-agent-sdk-task-events.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, test } from "bun:test"; + +import type { SDKBackgroundTasksChangedMessage } from "@anthropic-ai/claude-agent-sdk"; + +import type { DriverEventInput } from "../src/protocol/events"; +import { ClaudeAgentSdkTaskEvents } from "../src/runtimes/claude/agent-sdk-task-events"; + +function backgroundTasks( + tasks: SDKBackgroundTasksChangedMessage["tasks"], +): SDKBackgroundTasksChangedMessage { + return { + session_id: "session-1", + subtype: "background_tasks_changed", + tasks, + type: "system", + uuid: "00000000-0000-0000-0000-000000000001", + }; +} + +function task( + taskId: string, + description = "Inspect the repository", + ambient = false, +): SDKBackgroundTasksChangedMessage["tasks"][number] { + return { + ...(ambient ? { ambient: true } : {}), + description, + task_id: taskId, + task_type: "local_agent", + }; +} + +function commit( + projection: ClaudeAgentSdkTaskEvents, + message: SDKBackgroundTasksChangedMessage, +): readonly DriverEventInput[] { + const prepared = projection.prepare(message); + prepared.commit(); + return prepared.events; +} + +describe("Claude Agent SDK task events", () => { + test("projects replacement snapshots and metadata changes", () => { + const projection = new ClaudeAgentSdkTaskEvents(); + + expect(commit(projection, backgroundTasks([task("task-1")]))).toEqual([ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { + active: true, + taskId: "task-1", + taskType: "local_agent", + title: "Inspect the repository", + }, + }, + ]); + expect(commit(projection, backgroundTasks([task("task-1")]))).toEqual([]); + expect(commit(projection, backgroundTasks([task("task-1", "Inspect tests")]))).toEqual([ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { taskId: "task-1", title: "Inspect tests" }, + }, + ]); + }); + + test("treats removal and ambient changes as visibility, not guessed completion", () => { + const projection = new ClaudeAgentSdkTaskEvents(); + commit(projection, backgroundTasks([task("task-1")])); + + expect(commit(projection, backgroundTasks([task("task-1", undefined, true)]))).toEqual([ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { active: false, taskId: "task-1" }, + }, + ]); + expect(commit(projection, backgroundTasks([task("task-1")]))[0]).toMatchObject({ + payload: { active: true, taskId: "task-1" }, + }); + expect(commit(projection, backgroundTasks([]))).toEqual([ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { active: false, taskId: "task-1" }, + }, + ]); + }); + + test("bounds visible tasks, IDs, and text", () => { + const projection = new ClaudeAgentSdkTaskEvents(); + const events = commit( + projection, + backgroundTasks( + Array.from({ length: 256 }, (_, index) => + task(index === 0 ? "界".repeat(257) : `task-${index}`, `${"x".repeat(4_095)}😀tail`), + ), + ), + ); + + expect(events).toHaveLength(256); + expect(events[0]?.payload).toMatchObject({ + taskId: expect.stringMatching(/^claude-task:[a-f0-9]{64}$/), + title: expect.stringMatching(/^x{4095}$/), + }); + expect(projection.prepareClosure().events).toHaveLength(256); + }); + + test("filters ambient prefixes and omits empty metadata", () => { + const projection = new ClaudeAgentSdkTaskEvents(); + const ambient = Array.from({ length: 1_023 }, (_, index) => + task(`ambient-${index}`, "private", true), + ); + + expect( + commit( + projection, + backgroundTasks([...ambient, { description: "", task_id: "visible", task_type: "" }]), + ), + ).toEqual([ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { active: true, taskId: "visible" }, + }, + ]); + + const oversized = projection.prepare( + backgroundTasks(Array.from({ length: 257 }, (_, index) => task(`visible-${index}`))), + ); + expect(oversized.events).toMatchObject([ + { + kind: "diagnostic.reported", + payload: { code: "claude.visible_background_tasks_too_many" }, + visibility: "owner_debug", + }, + ]); + oversized.commit(); + expect(projection.prepareClosure().events).toHaveLength(1); + + expect( + projection.prepare( + backgroundTasks( + Array.from({ length: 1_025 }, (_, index) => task(`raw-${index}`, "x", true)), + ), + ).events, + ).toMatchObject([ + { + kind: "diagnostic.reported", + payload: { code: "claude.background_tasks_snapshot_too_large" }, + }, + ]); + expect(projection.prepareClosure().events).toHaveLength(1); + }); + + test("retains only the durably accepted prefix", () => { + const projection = new ClaudeAgentSdkTaskEvents(); + const prepared = projection.prepare(backgroundTasks([task("task-1"), task("task-2")])); + + expect(prepared.events).toHaveLength(2); + prepared.beforePublish(0); + prepared.commitAccepted(0); + expect(projection.prepareClosure().events).toEqual([ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { active: false, taskId: "task-1" }, + }, + ]); + }); + + test("commits closure only after durable publication", () => { + const projection = new ClaudeAgentSdkTaskEvents(); + commit(projection, backgroundTasks([task("task-1")])); + + const closure = projection.prepareClosure(); + expect(closure.events).toEqual([ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { active: false, taskId: "task-1" }, + }, + ]); + expect(projection.prepareClosure().events).toEqual(closure.events); + closure.commit(); + expect(projection.prepareClosure().events).toEqual([]); + }); +}); diff --git a/tests/claude-agent-sdk-test-helpers.ts b/tests/claude-agent-sdk-test-helpers.ts new file mode 100644 index 0000000..6b68e42 --- /dev/null +++ b/tests/claude-agent-sdk-test-helpers.ts @@ -0,0 +1,34 @@ +import type { DriverEventInput } from "../src/protocol/events"; +import { isRecord } from "../src/runtimes/claude/agent-sdk-json"; + +export { isRecord } from "../src/runtimes/claude/agent-sdk-json"; + +export function messageText(events: readonly DriverEventInput[], messageId: unknown): string { + let text = ""; + + for (const event of events) { + if (!isRecord(event.payload) || event.payload["messageId"] !== messageId) { + continue; + } + + if (event.kind === "message.added") { + const content = event.payload["content"]; + text = Array.isArray(content) + ? content + .flatMap((block) => + isRecord(block) && typeof block["text"] === "string" ? [block["text"]] : [], + ) + .join("") + : typeof content === "string" + ? content + : ""; + } else if ( + event.kind === "message.delta" && + typeof event.payload["contentDelta"] === "string" + ) { + text += event.payload["contentDelta"]; + } + } + + return text; +} diff --git a/tests/claude-contract-adapter-permission.test.ts b/tests/claude-contract-adapter-permission.test.ts deleted file mode 100644 index 809028b..0000000 --- a/tests/claude-contract-adapter-permission.test.ts +++ /dev/null @@ -1,500 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - Run, - SessionSnapshot, -} from "../src/contract"; -import { ClaudeContractAdapter } from "../src/runtimes/claude/contract-adapter"; -import type { - ContractAuthorityUpdate, - ContractPreviewUpdate, -} from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - "interaction.permission": {}, - "item.artifact": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - interactionTimeoutMs = 5 * 60 * 1_000, - maxToolInputBytes?: number, - maxPendingPermissionBytes?: number, - onAuthority?: (update: ContractAuthorityUpdate) => Promise | void, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - const authority: ContractAuthorityUpdate[] = []; - const previews: ContractPreviewUpdate[] = []; - const commit = (cause: CommittedMutation["cause"], operations: AuthorityOperation[]): void => { - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId: protocolId(1_000 + revision), - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - }; - const adapter = new ClaudeContractAdapter({ - authority: async (update) => { - authority.push(update); - await onAuthority?.(update); - commit(update.cause, [...update.operations] as AuthorityOperation[]); - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs, - maxPendingPermissionBytes, - maxToolInputBytes, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - previews, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: resolution.value, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -async function registerRun(adapter: ClaudeContractAdapter): Promise { - adapter.attachRun(activeRun("2026-07-16T08:00:00.000Z")); -} - -function permissionOptions(requestId: string, toolUseID: string) { - return { - requestId, - signal: new AbortController().signal, - title: "Run command?", - toolUseID, - } satisfies Parameters[2]; -} - -function permissionBytes( - input: Record, - options: Parameters[2], -): number { - return new TextEncoder().encode( - JSON.stringify({ - input, - options: Object.fromEntries(Object.entries(options).filter(([name]) => name !== "signal")), - toolName: "Bash", - }), - ).byteLength; -} - -describe("Claude Contract adapter", () => { - test("bounds pending permission payloads and restores budget after resolution", async () => { - const input = { command: "pwd" }; - const firstOptions = permissionOptions("request-1", "tool-1"); - const secondOptions = permissionOptions("request-2", "tool-2"); - const maxPendingPermissionBytes = permissionBytes(input, firstOptions); - const harness = createHarness(5 * 60 * 1_000, undefined, maxPendingPermissionBytes); - await registerRun(harness.adapter); - const interactionId = await harness.adapter.openPermission(RUN_ID, "Bash", input, firstOptions); - const authorityCount = harness.authority.length; - - await expect( - harness.adapter.openPermission(RUN_ID, "Bash", input, secondOptions), - ).rejects.toThrow("pending permission budget"); - expect(harness.authority).toHaveLength(authorityCount); - - const resolution = { - kind: "permission", - value: { type: "cancelled" }, - } satisfies InteractionResolution; - harness.settleInteraction(interactionId, resolution); - await harness.adapter.resolveInteraction(interactionId, resolution); - await expect( - harness.adapter.openPermission(RUN_ID, "Bash", input, secondOptions), - ).resolves.toBeDefined(); - }); - - test("reserves permission budget before the first Authority await", async () => { - const input = { command: "pwd" }; - const firstOptions = permissionOptions("request-concurrent-1", "tool-concurrent-1"); - const secondOptions = permissionOptions("request-concurrent-2", "tool-concurrent-2"); - const firstAuthority = Promise.withResolvers(); - const releaseAuthority = Promise.withResolvers(); - let toolCommits = 0; - const harness = createHarness( - 5 * 60 * 1_000, - undefined, - permissionBytes(input, firstOptions), - async (update) => { - if (update.event === "permission/requested.tool" && ++toolCommits === 1) { - firstAuthority.resolve(); - await releaseAuthority.promise; - } - }, - ); - await registerRun(harness.adapter); - const first = harness.adapter.openPermission(RUN_ID, "Bash", input, firstOptions); - await firstAuthority.promise; - - try { - await expect( - harness.adapter.openPermission(RUN_ID, "Bash", input, secondOptions), - ).rejects.toThrow("pending permission budget"); - } finally { - releaseAuthority.resolve(); - } - - await expect(first).resolves.toBeDefined(); - expect(toolCommits).toBe(1); - }); - - test("rolls back a permission reservation when Authority rejects it", async () => { - const input = { command: "pwd" }; - const firstOptions = permissionOptions("request-failed", "tool-failed"); - const secondOptions = permissionOptions("request-retry", "tool-retry"); - let rejectNext = true; - const harness = createHarness( - 5 * 60 * 1_000, - undefined, - permissionBytes(input, firstOptions), - (update) => { - if (rejectNext && update.event === "permission/requested.tool") { - rejectNext = false; - throw new Error("Authority unavailable"); - } - }, - ); - await registerRun(harness.adapter); - - await expect( - harness.adapter.openPermission(RUN_ID, "Bash", input, firstOptions), - ).rejects.toThrow("Authority unavailable"); - await expect( - harness.adapter.openPermission(RUN_ID, "Bash", input, secondOptions), - ).resolves.toBeDefined(); - }); - - test.each(["permission/requested.tool", "permission/requested"] as const)( - "retries the exact %s write after an unknown Authority outcome", - async (event) => { - const writes: ContractAuthorityUpdate[] = []; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, (update) => { - if (update.event !== event) { - return; - } - - writes.push(update); - if (writes.length === 1) { - throw new AuthorityOutcomeUnknownError("Authority response was lost"); - } - }); - await registerRun(harness.adapter); - - await expect( - harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - permissionOptions(`request-unknown-${event}`, `tool-unknown-${event}`), - ), - ).resolves.toBeDefined(); - - expect(writes).toHaveLength(2); - expect(writes[1]?.mutationId).toBe(writes[0]?.mutationId); - expect(writes[1]?.operations).toEqual(writes[0]?.operations); - expect(harness.snapshot().interactions).toHaveLength(1); - }, - ); - - test("coalesces concurrent redelivery of the same permission request", async () => { - const input = { command: "pwd" }; - const options = permissionOptions("request-redelivered", "tool-redelivered"); - const firstAuthority = Promise.withResolvers(); - const releaseAuthority = Promise.withResolvers(); - let toolCommits = 0; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event === "permission/requested.tool" && ++toolCommits === 1) { - firstAuthority.resolve(); - await releaseAuthority.promise; - } - }); - await registerRun(harness.adapter); - const first = harness.adapter.openPermission(RUN_ID, "Bash", input, options); - await firstAuthority.promise; - const replay = harness.adapter.openPermission( - RUN_ID, - "Bash", - { ...input }, - { - ...options, - signal: new AbortController().signal, - }, - ); - releaseAuthority.resolve(); - - const [interactionId, replayedInteractionId] = await Promise.all([first, replay]); - expect(replayedInteractionId).toBe(interactionId); - expect(toolCommits).toBe(1); - expect(harness.snapshot().interactions).toHaveLength(1); - }); - - test("cancels an opening permission when a replay signal aborts", async () => { - const interactionWriting = Promise.withResolvers(); - const releaseInteraction = Promise.withResolvers(); - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event === "permission/requested") { - interactionWriting.resolve(); - await releaseInteraction.promise; - } - }); - await registerRun(harness.adapter); - const firstController = new AbortController(); - const replayController = new AbortController(); - const options = { - ...permissionOptions("request-opening-replay", "tool-opening-replay"), - signal: firstController.signal, - }; - const first = harness.adapter.openPermission(RUN_ID, "Bash", { command: "pwd" }, options); - await interactionWriting.promise; - const replay = harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - { - ...options, - signal: replayController.signal, - }, - ); - - replayController.abort(); - releaseInteraction.resolve(); - - expect(await Promise.allSettled([first, replay])).toMatchObject([ - { reason: { name: "AbortError" }, status: "rejected" }, - { reason: { name: "AbortError" }, status: "rejected" }, - ]); - expect(harness.snapshot().interactions).toMatchObject([ - { resolution: { type: "cancelled" }, status: "resolved" }, - ]); - }); - - test("does not allow a pending permission after a replay signal aborts", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - const options = permissionOptions("request-pending-replay", "tool-pending-replay"); - const interactionId = await harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - options, - ); - const replayController = new AbortController(); - await harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - { - ...options, - signal: replayController.signal, - }, - ); - const interaction = harness.snapshot().interactions[0]; - const allowOnceId = - interaction?.kind === "permission" - ? interaction.request.options.find( - (option) => option.effect === "allow" && option.scope === "once", - )?.id - : undefined; - - replayController.abort(); - await expect( - harness.adapter.resolveInteraction(interactionId, { - kind: "permission", - value: { optionId: allowOnceId!, type: "selected" }, - }), - ).resolves.toBeNull(); - expect(harness.snapshot().interactions[0]).toMatchObject({ - resolution: { type: "cancelled" }, - status: "resolved", - }); - }); - - test("rejects an already-aborted permission without creating Authority state", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - const controller = new AbortController(); - controller.abort(); - - await expect( - harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - { - requestId: "request-aborted", - signal: controller.signal, - toolUseID: "tool-aborted", - }, - ), - ).rejects.toMatchObject({ name: "AbortError" }); - expect(harness.authority).toHaveLength(0); - expect(harness.snapshot().items).toHaveLength(0); - expect(harness.snapshot().interactions).toHaveLength(0); - }); - - test("resolves and releases a pending permission when its SDK signal aborts", async () => { - const input = { command: "pwd" }; - const controller = new AbortController(); - const options = { - ...permissionOptions("request-abort-pending", "tool-abort-pending"), - signal: controller.signal, - }; - const harness = createHarness(5 * 60 * 1_000, undefined, permissionBytes(input, options)); - await registerRun(harness.adapter); - const interactionId = await harness.adapter.openPermission(RUN_ID, "Bash", input, options); - - controller.abort(); - await expect( - harness.adapter.resolveInteraction(interactionId, { - kind: "permission", - value: { type: "cancelled" }, - }), - ).resolves.toBeNull(); - - expect(harness.snapshot().interactions[0]).toMatchObject({ - id: interactionId, - resolution: { type: "cancelled" }, - status: "resolved", - }); - await expect( - harness.adapter.openPermission( - RUN_ID, - "Bash", - input, - permissionOptions("request-abort-reuse", "tool-abort-reuse"), - ), - ).resolves.toBeDefined(); - }); - - test("retries an aborted permission after a rejected Authority write", async () => { - const controller = new AbortController(); - const firstAbort = Promise.withResolvers(); - let rejectAbort = true; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, (update) => { - if (update.event === "permission/aborted" && rejectAbort) { - rejectAbort = false; - firstAbort.resolve(); - throw new Error("Authority unavailable"); - } - }); - await registerRun(harness.adapter); - const interactionId = await harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - { - requestId: "request-abort-retry", - signal: controller.signal, - toolUseID: "tool-abort-retry", - }, - ); - - controller.abort(); - await firstAbort.promise; - await Promise.resolve(); - await Promise.resolve(); - expect(harness.snapshot().interactions[0]?.status).toBe("open"); - - await expect( - harness.adapter.resolveInteraction(interactionId, { - kind: "permission", - value: { type: "cancelled" }, - }), - ).resolves.toBeNull(); - expect(harness.snapshot().interactions[0]).toMatchObject({ - resolution: { type: "cancelled" }, - status: "resolved", - }); - }); -}); diff --git a/tests/claude-contract-adapter-run-lifecycle.test.ts b/tests/claude-contract-adapter-run-lifecycle.test.ts deleted file mode 100644 index ecd4cb2..0000000 --- a/tests/claude-contract-adapter-run-lifecycle.test.ts +++ /dev/null @@ -1,523 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; - -import { - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - Run, - SessionSnapshot, -} from "../src/contract"; -import { ClaudeContractAdapter } from "../src/runtimes/claude/contract-adapter"; -import type { - ContractAuthorityUpdate, - ContractPreviewUpdate, -} from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function sdkMessage(value: unknown): SDKMessage { - return value as SDKMessage; -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - "interaction.permission": {}, - "item.artifact": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - interactionTimeoutMs = 5 * 60 * 1_000, - maxToolInputBytes?: number, - maxPendingPermissionBytes?: number, - onAuthority?: (update: ContractAuthorityUpdate) => Promise | void, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - const authority: ContractAuthorityUpdate[] = []; - const previews: ContractPreviewUpdate[] = []; - const commit = (cause: CommittedMutation["cause"], operations: AuthorityOperation[]): void => { - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId: protocolId(1_000 + revision), - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - }; - const adapter = new ClaudeContractAdapter({ - authority: async (update) => { - authority.push(update); - await onAuthority?.(update); - commit(update.cause, [...update.operations] as AuthorityOperation[]); - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs, - maxPendingPermissionBytes, - maxToolInputBytes, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - previews, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: resolution.value, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -async function registerRun(adapter: ClaudeContractAdapter): Promise { - adapter.attachRun(activeRun("2026-07-16T08:00:00.000Z")); -} - -describe("Claude Contract adapter", () => { - test("does not let a late stream stop overwrite authoritative tool input", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleMessage( - sdkMessage({ - event: { - content_block: { id: "tool-1", input: {}, name: "Read", type: "tool_use" }, - index: 0, - type: "content_block_start", - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - event: { - delta: { partial_json: '{"value":1}', type: "input_json_delta" }, - index: 0, - type: "content_block_delta", - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - message: { - content: [{ id: "tool-1", input: { value: 2 }, name: "Read", type: "tool_use" }], - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "assistant", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - event: { index: 0, type: "content_block_stop" }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - - expect(harness.snapshot().items).toContainEqual( - expect.objectContaining({ id: "tool:tool-1", input: { value: 2 } }), - ); - }); - - test("rejects oversized permission input before creating Authority state", async () => { - const harness = createHarness(5 * 60 * 1_000, 8); - await registerRun(harness.adapter); - - await expect( - harness.adapter.openPermission( - RUN_ID, - "Bash", - { payload: "too-large" }, - { - requestId: "oversized-request", - signal: new AbortController().signal, - toolUseID: "tool-oversized", - }, - ), - ).rejects.toThrow("exceeds its byte limit"); - expect(harness.snapshot().items).toHaveLength(0); - expect(harness.snapshot().interactions).toHaveLength(0); - }); - - test.each([Number.POSITIVE_INFINITY, 1.5])("rejects invalid tool input limit %p", (value) => { - expect(() => createHarness(5 * 60 * 1_000, value)).toThrow( - "limits must be finite and positive", - ); - }); - - test.each([Number.POSITIVE_INFINITY, 1.5])( - "rejects invalid pending permission limit %p", - (value) => { - expect(() => createHarness(5 * 60 * 1_000, undefined, value)).toThrow( - "limits must be finite and positive", - ); - }, - ); - - test("preserves first-seen assistant block order", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleMessage( - sdkMessage({ - message: { - content: [ - { text: "before tool", type: "text" }, - { id: "tool-1", input: {}, name: "Read", type: "tool_use" }, - { thinking: "after tool", type: "thinking" }, - ], - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "assistant", - uuid: "assistant-1", - }), - RUN_ID, - ); - - expect(harness.snapshot().items.map((item) => item.kind)).toEqual([ - "message", - "tool", - "reasoning", - ]); - }); - - test("isolates streamed tool indexes by assistant and drops invalid JSON fragments", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - - for (const [uuid, toolId] of [ - ["assistant-a", "tool-a"], - ["assistant-b", "tool-b"], - ["assistant-c", "tool-c"], - ] as const) { - await harness.adapter.handleMessage( - sdkMessage({ - event: { - content_block: { id: toolId, name: "Read", type: "tool_use" }, - index: 0, - type: "content_block_start", - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid, - }), - RUN_ID, - ); - } - - for (const [uuid, partialJson] of [ - ["assistant-a", '{"a":1}'], - ["assistant-b", '{"b":2}'], - ["assistant-c", "{"], - ] as const) { - await harness.adapter.handleMessage( - sdkMessage({ - event: { - delta: { partial_json: partialJson, type: "input_json_delta" }, - index: 0, - type: "content_block_delta", - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid, - }), - RUN_ID, - ); - } - - for (const uuid of ["assistant-a", "assistant-b", "assistant-c"] as const) { - await harness.adapter.handleMessage( - sdkMessage({ - event: { index: 0, type: "content_block_stop" }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid, - }), - RUN_ID, - ); - } - - expect(harness.snapshot().items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: "tool:tool-a", input: { a: 1 } }), - expect.objectContaining({ id: "tool:tool-b", input: { b: 2 } }), - ]), - ); - const invalid = harness.snapshot().items.find((item) => item.id === "tool:tool-c"); - expect(invalid).toMatchObject({ id: "tool:tool-c", kind: "tool" }); - expect(invalid?.kind === "tool" ? invalid.input : null).toBeUndefined(); - }); - - test("avoids phantom messages and cancels unresolved items at a successful result boundary", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleMessage( - sdkMessage({ - event: { type: "message_start" }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-empty", - }), - RUN_ID, - ); - expect(harness.snapshot().items).toHaveLength(0); - - await harness.adapter.handleMessage( - sdkMessage({ - elapsed_time_seconds: 1, - session_id: "native-session-1", - tool_name: "Read", - tool_use_id: "tool-pending", - type: "tool_progress", - uuid: "progress-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - is_error: false, - modelUsage: {}, - num_turns: 1, - permission_denials: [], - result: "done", - session_id: "native-session-1", - stop_reason: null, - subtype: "success", - terminal_reason: "background_requested", - total_cost_usd: 0, - type: "result", - usage: { input_tokens: 1, output_tokens: 1 }, - uuid: "result-1", - }), - RUN_ID, - ); - - expect(harness.snapshot().runs[0]).toMatchObject({ - finishReason: "other", - status: "completed", - }); - expect(harness.snapshot().items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: "tool:tool-pending", status: "cancelled" }), - expect.objectContaining({ content: [{ text: "done", type: "text" }], status: "completed" }), - ]), - ); - }); - - test("fences native sessions", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleMessage( - sdkMessage({ - mcp_servers: [], - model: "sonnet", - permissionMode: "default", - session_id: "native-session-1", - subtype: "init", - tools: [], - type: "system", - uuid: "init-1", - }), - RUN_ID, - ); - await expect( - harness.adapter.handleMessage( - sdkMessage({ - mcp_servers: [], - model: "sonnet", - permissionMode: "default", - session_id: "native-session-2", - subtype: "init", - tools: [], - type: "system", - uuid: "init-2", - }), - RUN_ID, - ), - ).rejects.toThrow("different native session"); - }); - - test.each([ - ["error_max_turns", "max_turns"], - ["error_max_budget_usd", "budget_exhausted"], - ["error_max_structured_output_retries", "structured_output_retry_exhausted"], - ] as const)("treats %s as a limit", async (subtype, terminalReason) => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleMessage( - sdkMessage({ - errors: [`Run stopped at ${terminalReason}`], - is_error: true, - modelUsage: {}, - num_turns: 10, - permission_denials: [], - session_id: "native-session-1", - stop_reason: null, - subtype, - terminal_reason: terminalReason, - total_cost_usd: 0, - type: "result", - usage: { input_tokens: 1, output_tokens: 1 }, - uuid: "result-1", - }), - RUN_ID, - ); - - expect(harness.snapshot().runs[0]).toMatchObject({ - finishReason: "limit", - status: "completed", - }); - }); - - test("waits for task notification before making a task immutable", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleMessage( - sdkMessage({ - description: "Research", - session_id: "native-session-1", - subtype: "task_started", - task_id: "task-1", - type: "system", - uuid: "task-start-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - description: "Research replay", - session_id: "native-session-1", - subtype: "task_started", - task_id: "task-1", - type: "system", - uuid: "task-start-2", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - patch: { status: "completed" }, - session_id: "native-session-1", - subtype: "task_updated", - task_id: "task-1", - type: "system", - uuid: "task-update-1", - }), - RUN_ID, - ); - expect(harness.snapshot().items[0]?.status).toBe("active"); - - await harness.adapter.handleMessage( - sdkMessage({ - output_file: "/tmp/task-1.output", - session_id: "native-session-1", - status: "completed", - subtype: "task_notification", - summary: "Research complete", - task_id: "task-1", - type: "system", - usage: { duration_ms: 10, tool_uses: 1, total_tokens: 20 }, - uuid: "task-result-1", - }), - RUN_ID, - ); - expect(harness.snapshot().items[0]).toMatchObject({ - output: [{ text: "Research complete", type: "text" }], - status: "completed", - }); - }); -}); diff --git a/tests/claude-contract-adapter-terminal.test.ts b/tests/claude-contract-adapter-terminal.test.ts deleted file mode 100644 index 6170072..0000000 --- a/tests/claude-contract-adapter-terminal.test.ts +++ /dev/null @@ -1,516 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { CanUseTool, SDKMessage } from "@anthropic-ai/claude-agent-sdk"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - Run, - SessionSnapshot, -} from "../src/contract"; -import { ClaudeContractAdapter } from "../src/runtimes/claude/contract-adapter"; -import type { - ContractAuthorityUpdate, - ContractPreviewUpdate, -} from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function sdkMessage(value: unknown): SDKMessage { - return value as SDKMessage; -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - "interaction.permission": {}, - "item.artifact": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - interactionTimeoutMs = 5 * 60 * 1_000, - maxToolInputBytes?: number, - maxPendingPermissionBytes?: number, - onAuthority?: (update: ContractAuthorityUpdate) => Promise | void, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - const authority: ContractAuthorityUpdate[] = []; - const previews: ContractPreviewUpdate[] = []; - const commit = (cause: CommittedMutation["cause"], operations: AuthorityOperation[]): void => { - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId: protocolId(1_000 + revision), - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - }; - const adapter = new ClaudeContractAdapter({ - authority: async (update) => { - authority.push(update); - await onAuthority?.(update); - commit(update.cause, [...update.operations] as AuthorityOperation[]); - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs, - maxPendingPermissionBytes, - maxToolInputBytes, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - previews, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: resolution.value, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -async function registerRun(adapter: ClaudeContractAdapter): Promise { - adapter.attachRun(activeRun("2026-07-16T08:00:00.000Z")); -} - -function permissionOptions(requestId: string, toolUseID: string) { - return { - requestId, - signal: new AbortController().signal, - title: "Run command?", - toolUseID, - } satisfies Parameters[2]; -} - -function permissionBytes( - input: Record, - options: Parameters[2], -): number { - return new TextEncoder().encode( - JSON.stringify({ - input, - options: Object.fromEntries(Object.entries(options).filter(([name]) => name !== "signal")), - toolName: "Bash", - }), - ).byteLength; -} - -describe("Claude Contract adapter", () => { - test("retries the exact cancelled snapshot after an unknown Authority outcome", async () => { - const controller = new AbortController(); - const firstAbort = Promise.withResolvers(); - const aborts: ContractAuthorityUpdate[] = []; - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, (update) => { - if (update.event !== "permission/aborted") { - return; - } - - aborts.push(update); - if (aborts.length === 1) { - firstAbort.resolve(); - throw new AuthorityOutcomeUnknownError("Authority response was lost"); - } - }); - await registerRun(harness.adapter); - const interactionId = await harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - { - requestId: "request-abort-unknown", - signal: controller.signal, - toolUseID: "tool-abort-unknown", - }, - ); - - controller.abort(); - await firstAbort.promise; - await Promise.resolve(); - await expect( - harness.adapter.resolveInteraction(interactionId, { - kind: "permission", - value: { type: "cancelled" }, - }), - ).resolves.toBeNull(); - - expect(aborts).toHaveLength(2); - expect(aborts[1]?.mutationId).toBe(aborts[0]?.mutationId); - expect(aborts[1]?.operations).toEqual(aborts[0]?.operations); - }); - - test("converges a permission aborted during its opening Authority write", async () => { - const interactionWriting = Promise.withResolvers(); - const releaseInteraction = Promise.withResolvers(); - const aborted = Promise.withResolvers(); - const harness = createHarness(5 * 60 * 1_000, undefined, undefined, async (update) => { - if (update.event === "permission/requested") { - interactionWriting.resolve(); - await releaseInteraction.promise; - } - if (update.event === "permission/aborted") { - aborted.resolve(); - } - }); - await registerRun(harness.adapter); - const controller = new AbortController(); - const opening = harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - { - requestId: "request-abort-opening", - signal: controller.signal, - toolUseID: "tool-abort-opening", - }, - ); - await interactionWriting.promise; - - controller.abort(); - releaseInteraction.resolve(); - await expect(opening).rejects.toMatchObject({ name: "AbortError" }); - await aborted.promise; - expect(harness.snapshot().interactions[0]).toMatchObject({ - resolution: { type: "cancelled" }, - status: "resolved", - }); - }); - - test("retains an opening permission when its cancellation write is rejected", async () => { - const input = { command: "pwd" }; - const controller = new AbortController(); - const options = { - ...permissionOptions("request-orphan-1", "tool-orphan-1"), - signal: controller.signal, - }; - const interactionWriting = Promise.withResolvers(); - const releaseInteraction = Promise.withResolvers(); - let rejectCancellation = true; - const requested: ContractAuthorityUpdate[] = []; - const harness = createHarness( - 5 * 60 * 1_000, - undefined, - permissionBytes(input, options), - async (update) => { - if (update.event === "permission/requested") { - requested.push(update); - interactionWriting.resolve(); - await releaseInteraction.promise; - } - if (update.event === "permission/aborted" && rejectCancellation) { - rejectCancellation = false; - throw new Error("Authority unavailable"); - } - }, - ); - await registerRun(harness.adapter); - const opening = harness.adapter.openPermission(RUN_ID, "Bash", input, options); - await interactionWriting.promise; - - const retryOptions = permissionOptions("request-orphan-2", "tool-orphan-2"); - controller.abort(); - releaseInteraction.resolve(); - await expect(opening).rejects.toThrow("Authority unavailable"); - - const interactionId = harness.snapshot().interactions[0]?.id; - expect(interactionId).toBeDefined(); - await expect( - harness.adapter.openPermission(RUN_ID, "Bash", input, retryOptions), - ).rejects.toThrow("pending permission budget"); - expect(requested).toHaveLength(1); - await expect( - harness.adapter.resolveInteraction(interactionId!, { - kind: "permission", - value: { type: "cancelled" }, - }), - ).resolves.toBeNull(); - expect(harness.snapshot().interactions[0]).toMatchObject({ - id: interactionId, - resolution: { type: "cancelled" }, - status: "resolved", - }); - await expect( - harness.adapter.openPermission(RUN_ID, "Bash", input, retryOptions), - ).resolves.toBeDefined(); - }); - - test("releases an opening permission reservation when its run finishes", async () => { - const input = { command: "pwd" }; - const options = permissionOptions("request-late", "tool-late"); - const interactionAuthority = Promise.withResolvers(); - const releaseAuthority = Promise.withResolvers(); - const harness = createHarness( - 5 * 60 * 1_000, - undefined, - permissionBytes(input, options), - async (update) => { - if (update.event === "permission/requested") { - interactionAuthority.resolve(); - await releaseAuthority.promise; - } - }, - ); - await registerRun(harness.adapter); - const opening = harness.adapter.openPermission(RUN_ID, "Bash", input, options); - await interactionAuthority.promise; - - const finishing = harness.adapter.handleMessage( - sdkMessage({ - is_error: false, - modelUsage: {}, - num_turns: 1, - permission_denials: [], - result: "done", - session_id: "native-session-1", - stop_reason: "end_turn", - subtype: "success", - total_cost_usd: 0, - type: "result", - usage: { input_tokens: 1, output_tokens: 1 }, - uuid: "result-late-permission", - }), - RUN_ID, - ); - releaseAuthority.resolve(); - await finishing; - - await expect(opening).rejects.toThrow("active Run"); - await expect( - harness.adapter.openPermission( - RUN_ID, - "Bash", - input, - permissionOptions("request-next", "tool-next"), - ), - ).rejects.toThrow("unknown run"); - }); - - test("translates a Coordinator expiry without rechecking its local deadline", async () => { - const harness = createHarness(1_000); - await registerRun(harness.adapter); - const interactionId = await harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - { - requestId: "request-expired", - signal: new AbortController().signal, - toolUseID: "tool-expired", - }, - ); - harness.advance(1_001); - harness.settleInteraction(interactionId); - - await expect( - harness.adapter.resolveInteraction(interactionId, { - kind: "permission", - value: { type: "cancelled" }, - }), - ).resolves.toMatchObject({ behavior: "deny", interrupt: true }); - expect(harness.snapshot().interactions[0]).toMatchObject({ status: "expired" }); - }); - - test("turns aborted result frames into a cancelled run and flushes active Preview", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleMessage( - sdkMessage({ - event: { type: "message_start" }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - event: { - delta: { text: "partial", type: "text_delta" }, - index: 0, - type: "content_block_delta", - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - errors: ["aborted"], - is_error: true, - modelUsage: {}, - num_turns: 1, - permission_denials: [], - session_id: "native-session-1", - stop_reason: null, - subtype: "error_during_execution", - terminal_reason: "aborted_streaming", - total_cost_usd: 0, - type: "result", - usage: { input_tokens: 1, output_tokens: 1 }, - uuid: "result-1", - }), - RUN_ID, - ); - - expect(harness.snapshot().runs[0]?.status).toBe("cancelled"); - expect(harness.snapshot().items).toContainEqual( - expect.objectContaining({ - content: [{ text: "partial", type: "text" }], - kind: "message", - status: "cancelled", - }), - ); - }); - - test("bounds streamed and authoritative tool input and normalizes empty SDK labels", async () => { - const harness = createHarness(5 * 60 * 1_000, 8); - await registerRun(harness.adapter); - await harness.adapter.handleMessage( - sdkMessage({ - event: { - content_block: { id: "tool-1", input: {}, name: "", type: "tool_use" }, - index: 0, - type: "content_block_start", - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - event: { - delta: { partial_json: '{"payload":"too-large"}', type: "input_json_delta" }, - index: 0, - type: "content_block_delta", - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - event: { index: 0, type: "content_block_stop" }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - await expect( - harness.adapter.handleMessage( - sdkMessage({ - message: { - content: [ - { - id: "tool-1", - input: { payload: "authoritative" }, - name: "", - type: "tool_use", - }, - ], - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "assistant", - uuid: "assistant-1", - }), - RUN_ID, - ), - ).rejects.toThrow("exceeds its byte limit"); - - expect(harness.snapshot().items).toContainEqual( - expect.objectContaining({ - input: {}, - kind: "tool", - name: "Tool", - }), - ); - }); -}); diff --git a/tests/claude-contract-adapter-transcript.test.ts b/tests/claude-contract-adapter-transcript.test.ts deleted file mode 100644 index 0cc583d..0000000 --- a/tests/claude-contract-adapter-transcript.test.ts +++ /dev/null @@ -1,502 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { CanUseTool, SDKMessage } from "@anthropic-ai/claude-agent-sdk"; - -import { - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - Run, - SessionSnapshot, -} from "../src/contract"; -import { ClaudeContractAdapter } from "../src/runtimes/claude/contract-adapter"; -import type { - ContractAuthorityUpdate, - ContractPreviewUpdate, -} from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function sdkMessage(value: unknown): SDKMessage { - return value as SDKMessage; -} - -function resultMessage(usage: unknown, totalCostUsd: number): SDKMessage { - return sdkMessage({ - is_error: false, - modelUsage: {}, - num_turns: 1, - permission_denials: [], - result: "", - session_id: "native-session-1", - stop_reason: null, - subtype: "success", - total_cost_usd: totalCostUsd, - type: "result", - usage, - uuid: "result-1", - }); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - "interaction.permission": {}, - "item.artifact": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - interactionTimeoutMs = 5 * 60 * 1_000, - maxToolInputBytes?: number, - maxPendingPermissionBytes?: number, - onAuthority?: (update: ContractAuthorityUpdate) => Promise | void, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - const authority: ContractAuthorityUpdate[] = []; - const previews: ContractPreviewUpdate[] = []; - const commit = (cause: CommittedMutation["cause"], operations: AuthorityOperation[]): void => { - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId: protocolId(1_000 + revision), - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - }; - const adapter = new ClaudeContractAdapter({ - authority: async (update) => { - authority.push(update); - await onAuthority?.(update); - commit(update.cause, [...update.operations] as AuthorityOperation[]); - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs, - maxPendingPermissionBytes, - maxToolInputBytes, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - previews, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: resolution.value, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -async function registerRun(adapter: ClaudeContractAdapter): Promise { - adapter.attachRun(activeRun("2026-07-16T08:00:00.000Z")); -} - -describe("Claude Contract adapter", () => { - test("ignores model-call usage deltas until the Run-level result snapshot", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - const before = harness.authority.length; - - await harness.adapter.handleMessage( - sdkMessage({ - event: { - delta: { stop_reason: "end_turn", stop_sequence: null }, - type: "message_delta", - usage: { input_tokens: 10, output_tokens: 20 }, - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - - expect(harness.authority).toHaveLength(before); - expect(harness.snapshot().runs[0]?.usage).toBeUndefined(); - }); - - test.each([ - { - expected: undefined, - label: "MAX_VALUE input and output", - usage: { - cache_read_input_tokens: Number.MAX_VALUE, - input_tokens: Number.MAX_VALUE, - output_tokens: Number.MAX_VALUE, - }, - }, - { - expected: { cachedInput: 3, output: 2, total: 2 }, - label: "negative input", - usage: { cache_read_input_tokens: 3, input_tokens: -1, output_tokens: 2 }, - }, - { - expected: { cachedInput: 3, input: 2, total: 2 }, - label: "fractional output", - usage: { cache_read_input_tokens: 3, input_tokens: 2, output_tokens: 1.5 }, - }, - { - expected: { output: 2, total: 2 }, - label: "unsafe input and cached input", - usage: { - cache_read_input_tokens: Number.MAX_SAFE_INTEGER + 1, - input_tokens: Number.MAX_SAFE_INTEGER + 1, - output_tokens: 2, - }, - }, - { - expected: { input: Number.MAX_SAFE_INTEGER, output: 1 }, - label: "unsafe derived total", - usage: { - cache_read_input_tokens: Number.NaN, - input_tokens: Number.MAX_SAFE_INTEGER, - output_tokens: 1, - }, - }, - ])("drops $label without blocking Run completion", async ({ expected, usage }) => { - const harness = createHarness(); - await registerRun(harness.adapter); - - await harness.adapter.handleMessage(resultMessage(usage, Number.NaN), RUN_ID); - - expect(harness.snapshot().runs[0]).toMatchObject({ status: "completed" }); - expect(harness.snapshot().runs[0]?.usage).toEqual(expected); - }); - - test.each([ - { label: "negative", value: -0.01 }, - { label: "Infinity", value: Infinity }, - { label: "-Infinity", value: -Infinity }, - { label: "NaN", value: Number.NaN }, - ])("drops $label cost without blocking Run completion", async ({ value }) => { - const harness = createHarness(); - await registerRun(harness.adapter); - - await harness.adapter.handleMessage( - resultMessage({ input_tokens: 1, output_tokens: 2 }, value), - RUN_ID, - ); - - expect(harness.snapshot().runs[0]).toMatchObject({ - status: "completed", - usage: { input: 1, output: 2, total: 3 }, - }); - expect(harness.snapshot().runs[0]?.usage?.cost).toBeUndefined(); - }); - - test("repairs streamed text from the authoritative message and keeps source time as metadata", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - await harness.adapter.handleMessage( - sdkMessage({ - event: { type: "message_start" }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - event: { - delta: { text: "hel", type: "text_delta" }, - index: 0, - type: "content_block_delta", - }, - parent_tool_use_id: null, - session_id: "native-session-1", - type: "stream_event", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - message: { - content: [{ text: "hello", type: "text" }], - }, - parent_tool_use_id: null, - session_id: "native-session-1", - supersedes: ["assistant-old"], - timestamp: "2020-01-01T00:00:00.000Z", - type: "assistant", - uuid: "assistant-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - is_error: false, - modelUsage: {}, - num_turns: 1, - permission_denials: [], - result: "hello", - session_id: "native-session-1", - stop_reason: "max_tokens", - structured_output: { answer: 42 }, - subtype: "success", - total_cost_usd: 0.01, - type: "result", - usage: { - cache_read_input_tokens: 2, - input_tokens: 3, - output_tokens: 5, - }, - uuid: "result-1", - }), - RUN_ID, - ); - - expect(harness.previews.map((entry) => entry.update)).toMatchObject([ - { op: "append", text: "hel" }, - ]); - expect(harness.snapshot().items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - content: [{ text: "hello", type: "text" }], - extensions: { - "anthropic.agent-sdk/source-timestamp": "2020-01-01T00:00:00.000Z", - "anthropic.agent-sdk/supersedes": ["assistant-old"], - }, - id: "message:assistant-1", - kind: "message", - status: "completed", - }), - expect.objectContaining({ - content: [{ type: "json", value: { answer: 42 } }], - kind: "artifact", - name: "structured-output.json", - status: "completed", - }), - ]), - ); - expect(harness.snapshot().runs[0]).toMatchObject({ - finishReason: "limit", - status: "completed", - usage: { - cachedInput: 2, - cost: { amount: 0.01, currency: "USD" }, - input: 3, - output: 5, - total: 8, - }, - }); - }); - - test("round-trips permission decisions and keeps only session-scoped suggestions", async () => { - const harness = createHarness(); - await registerRun(harness.adapter); - const input = { command: "pwd" }; - const options = { - requestId: "request-1", - signal: new AbortController().signal, - suggestions: [ - { - behavior: "allow", - destination: "session", - rules: [{ toolName: "Bash" }], - type: "addRules", - }, - { - behavior: "allow", - destination: "userSettings", - rules: [{ toolName: "Bash" }], - type: "addRules", - }, - ], - title: "Run command?", - toolUseID: "tool-1", - } satisfies Parameters[2]; - const interactionId = await harness.adapter.openPermission(RUN_ID, "Bash", input, options); - expect( - await harness.adapter.openPermission( - RUN_ID, - "Bash", - { command: "pwd" }, - { - ...options, - signal: new AbortController().signal, - }, - ), - ).toBe(interactionId); - expect(harness.snapshot().interactions).toHaveLength(1); - const interaction = harness.snapshot().interactions.find((entry) => entry.id === interactionId); - const allowSessionId = - interaction?.kind === "permission" - ? interaction.request.options.find((option) => option.scope === "session")?.id - : undefined; - const resolution = { - kind: "permission", - value: { optionId: allowSessionId!, type: "selected" }, - } satisfies InteractionResolution; - - input.command = "rm -rf /"; - options.suggestions[0]!.rules[0]!.toolName = "Changed"; - harness.settleInteraction(interactionId, resolution); - await expect(harness.adapter.resolveInteraction(interactionId, resolution)).resolves.toEqual({ - behavior: "allow", - toolUseID: "tool-1", - updatedInput: { command: "pwd" }, - updatedPermissions: [ - { - behavior: "allow", - destination: "session", - rules: [{ toolName: "Bash" }], - type: "addRules", - }, - ], - }); - await harness.adapter.handleMessage( - sdkMessage({ - message: { - content: [ - { - content: [{ text: "/workspace", type: "text" }], - tool_use_id: "tool-1", - type: "tool_result", - }, - ], - }, - parent_tool_use_id: null, - session_id: "native-session-1", - timestamp: "2020-01-01T00:00:00.000Z", - type: "user", - uuid: "user-1", - }), - RUN_ID, - ); - await harness.adapter.handleMessage( - sdkMessage({ - is_error: false, - modelUsage: {}, - num_turns: 1, - permission_denials: [], - result: "done", - session_id: "native-session-1", - stop_reason: "end_turn", - subtype: "success", - total_cost_usd: 0, - type: "result", - usage: { input_tokens: 1, output_tokens: 1 }, - uuid: "result-1", - }), - RUN_ID, - ); - - expect(harness.snapshot().items).toContainEqual( - expect.objectContaining({ - id: "tool:tool-1", - kind: "tool", - output: [{ text: "/workspace", type: "text" }], - status: "completed", - }), - ); - expect(harness.snapshot().interactions[0]?.status).toBe("resolved"); - }); - - test.each([ - ["tool name", "Read", { command: "pwd" }, {}], - ["tool input", "Bash", { command: "rm -rf /" }, {}], - ["prompt metadata", "Bash", { command: "pwd" }, { title: "Changed prompt" }], - ] as const)( - "rejects a permission replay with changed %s", - async (_name, replayToolName, replayInput, optionChanges) => { - const harness = createHarness(); - await registerRun(harness.adapter); - const options = { - requestId: "request-replay", - signal: new AbortController().signal, - title: "Run command?", - toolUseID: "tool-replay", - } satisfies Parameters[2]; - await harness.adapter.openPermission(RUN_ID, "Bash", { command: "pwd" }, options); - const before = harness.authority.length; - - await expect( - harness.adapter.openPermission(RUN_ID, replayToolName, replayInput, { - ...options, - ...optionChanges, - }), - ).rejects.toThrow("changed identity"); - expect(harness.authority).toHaveLength(before); - expect(harness.snapshot().interactions).toHaveLength(1); - }, - ); -}); diff --git a/tests/cma-projection.test.ts b/tests/cma-projection.test.ts index edd3c21..5922c7a 100644 --- a/tests/cma-projection.test.ts +++ b/tests/cma-projection.test.ts @@ -2,8 +2,70 @@ import { describe, expect, test } from "bun:test"; import { CmaInvalidEventError, CmaUnsupportedFieldError } from "../src/projections/cma"; import { projectCmaInboundToDriverCommand, projectDriverEventToCma } from "../src/projections/cma"; +import { ingestRuntimeEventInput } from "../src/runtime-events"; +import { createDriverId } from "../src/protocol/id"; +import type { EventId, RunId, SessionId } from "../src/protocol/id"; describe("CMA projection", () => { + test("rejects malformed Claude terminal and structured payload fields", () => { + const context = { + createId: () => createDriverId() as EventId, + occurredAt: "2026-08-12T00:00:00.000Z", + sessionId: createDriverId() as SessionId, + } as const; + + for (const input of [ + { kind: "message.failed", payload: { messageId: "message-1" } }, + { + kind: "message.failed", + payload: { error: { code: "failed" }, messageId: "message-1" }, + }, + { + kind: "message.added", + payload: { + content: [{ text: "message", type: "text" }], + preventContinuation: "yes", + }, + }, + { kind: "message.added", payload: { content: "message", phase: "final_answer" } }, + { + kind: "message.added", + payload: { content: "message", memoryCitation: Symbol("bad") }, + }, + { + kind: "tool.call.updated", + payload: { nonExecutionKind: 1, status: "failed", toolCallId: "tool-1" }, + }, + { + kind: "tool.call.updated", + payload: { status: "completed", structuredOutput: Symbol("bad"), toolCallId: "tool-1" }, + }, + ]) { + expect(ingestRuntimeEventInput(context, input).status).toBe("rejected"); + } + }); + + test("admits message snapshot phase and memory citations", () => { + const context = { + createId: () => createDriverId() as EventId, + occurredAt: "2026-08-12T00:00:00.000Z", + sessionId: createDriverId() as SessionId, + } as const; + const memoryCitation = { + entries: [{ lineEnd: 2, lineStart: 1, path: "MEMORY.md" }], + threadIds: ["thread-1"], + }; + const result = ingestRuntimeEventInput(context, { + kind: "message.added", + payload: { content: "answer", memoryCitation, phase: "final" }, + }); + + expect(result).toMatchObject({ + event: { payload: { content: "answer", memoryCitation, phase: "final" } }, + status: "accepted", + }); + }); + test("projects user messages to input.start commands", () => { expect( projectCmaInboundToDriverCommand({ @@ -109,7 +171,16 @@ describe("CMA projection", () => { projectDriverEventToCma({ kind: "permission.requested", payload: { + agentId: "subagent-1", + blockedPath: "/workspace/secret", + decisionReason: "Path is outside the allowed roots.", details: '{"command":"vp test"}', + description: "Read access to /workspace/secret", + matchedAskRule: { + ruleContent: "Read(/workspace/secret/**)", + source: "project", + toolName: "Read", + }, requestId: "permission-1", targetItemId: "tool-1", title: "Approve command", @@ -122,7 +193,16 @@ describe("CMA projection", () => { ).toEqual([ { requiresAction: { + agentId: "subagent-1", + blockedPath: "/workspace/secret", + decisionReason: "Path is outside the allowed roots.", details: '{"command":"vp test"}', + description: "Read access to /workspace/secret", + matchedAskRule: { + ruleContent: "Read(/workspace/secret/**)", + source: "project", + toolName: "Read", + }, requestId: "permission-1", targetItemId: "tool-1", title: "Approve command", @@ -155,6 +235,43 @@ describe("CMA projection", () => { ).toEqual([]); }); + test("projects structured final output without flattening it into text", () => { + const runId = createDriverId() as RunId; + const context = { + createId: () => createDriverId() as EventId, + occurredAt: "2026-08-13T00:00:00.000Z", + runId, + sessionId: createDriverId() as SessionId, + } as const; + const event = { + kind: "run.completed" as const, + payload: { + stopReason: "end_turn", + structuredOutput: { answer: 42, citations: ["source-1"] }, + }, + runId, + }; + + expect(ingestRuntimeEventInput(context, event).status).toBe("accepted"); + expect( + ingestRuntimeEventInput(context, { + ...event, + payload: { ...event.payload, structuredOutput: Symbol("invalid") }, + }).status, + ).toBe("rejected"); + expect(projectDriverEventToCma(event)).toEqual([ + { + metadata: { + stopReason: "end_turn", + structuredOutput: { answer: 42, citations: ["source-1"] }, + }, + sessionStatus: "idle", + sourceEventKind: "run.completed", + type: "session.status_idle", + }, + ]); + }); + test("projects driver event families to CMA outbound events", () => { expect( projectDriverEventToCma({ @@ -170,18 +287,57 @@ describe("CMA projection", () => { type: "agent.message", }, ]); + expect( + projectDriverEventToCma({ + kind: "message.failed", + payload: { error: { code: "provider.failed" }, messageId: "message-1" }, + }), + ).toMatchObject([ + { + message: { error: { code: "provider.failed" }, messageId: "message-1" }, + sourceEventKind: "message.failed", + type: "agent.message", + }, + ]); + expect( + projectDriverEventToCma({ + kind: "tool.call.updated", + payload: { + status: "completed", + structuredOutput: { + usage: { output_tokens_details: { thinking_tokens: 3 } }, + }, + toolCallId: "tool-1", + }, + }), + ).toMatchObject([ + { + message: { + structuredOutput: { + usage: { output_tokens_details: { thinking_tokens: 3 } }, + }, + }, + type: "agent.tool_use", + }, + ]); expect( projectDriverEventToCma({ kind: "run.failed", payload: { error: { code: "driver.failed", + details: { apiErrorStatus: 529, terminalReason: "api_error" }, message: "failed", }, }, }), ).toMatchObject([ { + error: expect.objectContaining({ + error: expect.objectContaining({ + details: { apiErrorStatus: 529, terminalReason: "api_error" }, + }), + }), sessionStatus: "terminated", sourceEventKind: "run.failed", type: "session.error", @@ -191,6 +347,7 @@ describe("CMA projection", () => { projectDriverEventToCma({ kind: "usage.updated", payload: { + cachedWriteTokens: 3, inputTokens: 1, outputTokens: 2, }, @@ -199,6 +356,11 @@ describe("CMA projection", () => { { sourceEventKind: "usage.updated", type: "session.usage", + usage: { + cachedWriteTokens: 3, + inputTokens: 1, + outputTokens: 2, + }, }, ]); }); diff --git a/tests/contract-projection-authority.test.ts b/tests/contract-projection-authority.test.ts deleted file mode 100644 index 1ccbced..0000000 --- a/tests/contract-projection-authority.test.ts +++ /dev/null @@ -1,692 +0,0 @@ -import { expect, test } from "bun:test"; - -import { AuthorityOutcomeUnknownError, interactionSchema, itemSchema } from "../src/contract"; -import type { Item, Run } from "../src/contract"; -import { isDriverId } from "../src/protocol/id"; -import { - ContractProjection, - type ContractAuthorityUpdate, -} from "../src/runtimes/contract-projection"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const INTERACTION_ID = protocolId(3); - -function activeRun(startedAt: string, input = true): Run { - return { - id: RUN_ID, - input: input ? [{ text: "hello", type: "text" }] : [], - origin: input ? "user" : "system", - startedAt, - status: "active", - }; -} - -function activeMessage(id: string, timestamp: string): Extract { - return itemSchema.parse({ - audience: "participants", - content: [], - createdAt: timestamp, - id, - kind: "message", - role: "agent", - runId: RUN_ID, - status: "active", - updatedAt: timestamp, - }) as Extract; -} - -test.each([Number.NaN, 1.5])("Contract projection rejects invalid limit %p", (value) => { - expect( - () => - new ContractProjection({ - authority: async () => {}, - preview: () => {}, - previewCheckpointBytes: value, - sessionId: SESSION_ID, - }), - ).toThrow("finite and positive"); -}); - -test.each(["append", "replace"] as const)( - "Contract projection treats a throwing Preview callback as best-effort for %s", - async (mode) => { - const timestamp = "2026-07-16T08:00:00.000Z"; - let previews = 0; - const projection = new ContractProjection({ - authority: async () => {}, - preview: () => { - previews += 1; - throw new Error("subscriber failed"); - }, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - const item = - mode === "append" - ? activeMessage("item-1", timestamp) - : itemSchema.parse({ - audience: "participants", - createdAt: timestamp, - id: "item-1", - kind: "terminal", - runId: RUN_ID, - status: "active", - stderr: [], - stdout: [], - updatedAt: timestamp, - }); - await projection.putItem(RUN_ID, "item/started", { name: "item", type: "system" }, item); - - if (mode === "append") { - await expect( - projection.appendText({ - cause: { providerEventId: "delta-1", type: "provider" }, - channel: "message.text", - delta: "x", - event: "message/delta", - itemId: item.id, - runId: RUN_ID, - }), - ).resolves.toBeUndefined(); - expect(projection.materializedText(RUN_ID, item.id, "message.text")).toBe("x"); - } else { - await expect( - projection.replacePreview({ - channel: "terminal.stdout", - itemId: item.id, - runId: RUN_ID, - text: "x", - }), - ).resolves.toBeUndefined(); - expect(projection.materializedText(RUN_ID, item.id, "terminal.stdout")).toBe("x"); - } - - expect(previews).toBe(1); - }, -); - -test.each(["item", "interaction", "run"] as const)( - "Contract projection isolates an in-flight Authority %s submission", - async (entity) => { - const entered = Promise.withResolvers(); - const release = Promise.withResolvers(); - const timestamp = "2026-07-16T08:00:00.000Z"; - let captured: ContractAuthorityUpdate | undefined; - const projection = new ContractProjection({ - authority: async (update) => { - captured = update; - entered.resolve(); - await release.promise; - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - const interaction = interactionSchema.parse({ - audience: "participants", - blocking: true, - createdAt: timestamp, - expiresAt: "2026-07-16T08:05:00.000Z", - id: INTERACTION_ID, - kind: "permission", - request: { - options: [{ effect: "deny", id: "deny", label: "Deny", scope: "once" }], - subject: { operation: "execute", targets: ["workspace"], type: "resource" }, - title: "Run command?", - }, - runId: RUN_ID, - status: "open", - }); - const write = - entity === "item" - ? projection.putItem( - RUN_ID, - "item/put", - { name: "item", type: "system" }, - { - ...activeMessage("message-1", timestamp), - content: [ - { text: "safe", type: "text" }, - { data: "c2FmZQ==", mediaType: "text/plain", type: "inline_blob" }, - ], - }, - ) - : entity === "interaction" - ? projection.putInteraction( - RUN_ID, - "interaction/put", - { name: "interaction", type: "system" }, - interaction, - ) - : projection.updateUsage( - RUN_ID, - "run/usage", - { name: "usage", type: "system" }, - { cost: { amount: 1, currency: "USD" }, input: 1, total: 1 }, - ); - await entered.promise; - const corrupt = () => { - const operation = captured?.operations[0]; - - if (operation?.op !== "put") { - throw new Error("Expected a put operation."); - } - - if (operation.entity === "item" && operation.value.kind === "message") { - operation.value.role = "user"; - const text = operation.value.content[0]; - const blob = operation.value.content[1]; - if (text?.type === "text") { - text.text = "mutated"; - } - if (blob?.type === "inline_blob") { - blob.data = "bXV0YXRlZA=="; - } - } else if (operation.entity === "interaction" && operation.value.kind === "permission") { - operation.value.request.title = "Mutated"; - const option = operation.value.request.options[0]; - if (option !== undefined) { - option.label = "Mutated"; - } - } else if (operation.entity === "run" && operation.value.usage !== undefined) { - operation.value.usage.input = 999; - if (operation.value.usage.cost !== undefined) { - operation.value.usage.cost.amount = 999; - } - } - }; - corrupt(); - release.resolve(); - await write; - corrupt(); - - if (entity === "item") { - expect(projection.item(RUN_ID, "message-1")).toMatchObject({ - content: [ - { text: "safe", type: "text" }, - { data: "c2FmZQ==", type: "inline_blob" }, - ], - role: "agent", - }); - } else if (entity === "interaction") { - expect(projection.interaction(INTERACTION_ID)).toMatchObject({ - request: { options: [{ label: "Deny" }], title: "Run command?" }, - }); - } else { - expect(projection.run(RUN_ID)?.usage).toEqual({ - cost: { amount: 1, currency: "USD" }, - input: 1, - total: 1, - }); - } - }, -); - -test("Contract projection keeps its private Authority intent across a mutated unknown submission", async () => { - const writes: ContractAuthorityUpdate[] = []; - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async (update) => { - writes.push(update); - if (writes.length === 1) { - const operation = update.operations[0]; - if (operation?.op === "put" && operation.entity === "item") { - operation.value.updatedAt = "2026-07-16T08:00:01.000Z"; - } - throw new AuthorityOutcomeUnknownError("result lost"); - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - const item = activeMessage("message-1", timestamp); - const write = () => - projection.putItem(RUN_ID, "item/put", { name: "item", type: "system" }, item); - - await expect(write()).rejects.toThrow("result lost"); - await write(); - - expect(writes[1]?.mutationId).toBe(writes[0]?.mutationId); - expect(writes[1]?.operations).toMatchObject([ - { entity: "item", op: "put", value: { updatedAt: timestamp } }, - ]); - expect(projection.item(RUN_ID, item.id)?.updatedAt).toBe(timestamp); -}); - -test("Contract projection disposal preserves an in-flight result and fences queued writes", async () => { - const writeEntered = Promise.withResolvers(); - const releaseWrite = Promise.withResolvers(); - const events: string[] = []; - const projection = new ContractProjection({ - authority: async ({ event }) => { - events.push(event); - writeEntered.resolve(); - await releaseWrite.promise; - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun("2026-07-16T08:00:00.000Z")); - const write = projection.putItem( - RUN_ID, - "message/started", - { providerEventId: "message-1", type: "provider" }, - itemSchema.parse({ - audience: "participants", - content: [], - createdAt: "2026-07-16T08:00:00.000Z", - id: "message-1", - kind: "message", - role: "agent", - runId: RUN_ID, - status: "active", - updatedAt: "2026-07-16T08:00:00.000Z", - }), - ); - await writeEntered.promise; - const queued = projection.putItem( - RUN_ID, - "message/queued", - { providerEventId: "message-2", type: "provider" }, - activeMessage("message-2", "2026-07-16T08:00:00.000Z"), - ); - const queuedResult = queued.then( - () => ({ status: "fulfilled" as const }), - (reason: unknown) => ({ reason, status: "rejected" as const }), - ); - - projection.dispose(); - projection.dispose(); - expect( - await Promise.race([ - queuedResult, - new Promise((resolve) => setTimeout(() => resolve({ status: "pending" }), 10)), - ]), - ).toMatchObject({ reason: { message: "Contract projection is disposed." }, status: "rejected" }); - releaseWrite.resolve(); - - await expect(write).resolves.toMatchObject({ id: "message-1" }); - expect(await queuedResult).toMatchObject({ status: "rejected" }); - expect(events).toEqual(["message/started"]); - expect(() => projection.run(RUN_ID)).toThrow("disposed"); -}); - -test("Contract projection preserves an in-flight unknown Authority result after disposal", async () => { - const writeEntered = Promise.withResolvers(); - const releaseWrite = Promise.withResolvers(); - const projection = new ContractProjection({ - authority: async () => { - writeEntered.resolve(); - await releaseWrite.promise; - throw new AuthorityOutcomeUnknownError("result lost after disposal"); - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun("2026-07-16T08:00:00.000Z")); - const write = projection.putItem( - RUN_ID, - "message/started", - { providerEventId: "message-1", type: "provider" }, - activeMessage("message-1", "2026-07-16T08:00:00.000Z"), - ); - await writeEntered.promise; - projection.dispose(); - releaseWrite.resolve(); - - await expect(write).rejects.toMatchObject({ - message: "result lost after disposal", - name: "AuthorityOutcomeUnknownError", - }); -}); - -test("Contract projection bounds and releases its queued mutation count", async () => { - const writeEntered = Promise.withResolvers(); - const releaseWrite = Promise.withResolvers(); - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async ({ event }) => { - if (event === "message/active") { - writeEntered.resolve(); - await releaseWrite.promise; - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - const active = projection.putItem( - RUN_ID, - "message/active", - { name: "active", type: "system" }, - activeMessage("active", timestamp), - ); - await writeEntered.promise; - const queued = Array.from({ length: 1_023 }, (_, index) => - projection.putItem( - RUN_ID, - "message/queued", - { name: `queued-${index}`, type: "system" }, - activeMessage(`queued-${index}`, timestamp), - ), - ); - const queuedResults = Promise.allSettled(queued); - - await expect( - projection.putItem( - RUN_ID, - "message/overflow", - { name: "overflow", type: "system" }, - activeMessage("overflow", timestamp), - ), - ).rejects.toThrow("1024 entries"); - projection.dispose(); - expect((await queuedResults).every((result) => result.status === "rejected")).toBe(true); - releaseWrite.resolve(); - await expect(active).resolves.toMatchObject({ id: "active" }); -}); - -test("Contract projection bounds queued mutation UTF-8 bytes", async () => { - const writeEntered = Promise.withResolvers(); - const releaseWrite = Promise.withResolvers(); - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async ({ event }) => { - if (event === "message/active") { - writeEntered.resolve(); - await releaseWrite.promise; - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - await projection.putItem( - RUN_ID, - "message/started", - { name: "started", type: "system" }, - activeMessage("message-1", timestamp), - ); - const active = projection.putItem( - RUN_ID, - "message/active", - { name: "active", type: "system" }, - activeMessage("message-2", timestamp), - ); - await writeEntered.promise; - - await expect( - projection.appendText({ - cause: { providerEventId: "large-delta", type: "provider" }, - channel: "message.text", - delta: "x".repeat(32 * 1_024 * 1_024), - event: "message/delta", - itemId: "message-1", - runId: RUN_ID, - }), - ).rejects.toThrow("33554432 UTF-8 bytes"); - releaseWrite.resolve(); - await active; -}); - -test("Contract projection gives every Authority write a protocol mutation ID", async () => { - const mutationIds: string[] = []; - const projection = new ContractProjection({ - authority: async ({ mutationId }) => { - mutationIds.push(mutationId); - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun("2026-07-16T08:00:00.000Z")); - - await projection.putItem( - RUN_ID, - "message/started", - { providerEventId: "message-1", type: "provider" }, - activeMessage("message-1", "2026-07-16T08:00:00.000Z"), - ); - - expect(mutationIds).toHaveLength(1); - expect(isDriverId(mutationIds[0])).toBe(true); -}); - -test("Contract projection reuses a mutation ID after an ambiguous Authority failure", async () => { - const mutationIds: string[] = []; - const projection = new ContractProjection({ - authority: async ({ mutationId }) => { - mutationIds.push(mutationId); - - if (mutationIds.length === 1) { - throw new AuthorityOutcomeUnknownError("response lost after commit"); - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun("2026-07-16T08:00:00.000Z")); - const item = activeMessage("message-1", "2026-07-16T08:00:00.000Z"); - const write = () => - projection.putItem( - RUN_ID, - "message/started", - { providerEventId: "message-1", type: "provider" }, - item, - ); - - await expect(write()).rejects.toThrow("response lost"); - await expect( - projection.putItem( - RUN_ID, - "message/started", - { providerEventId: "message-1", type: "provider" }, - { ...item, content: [{ text: "changed", type: "text" }] }, - ), - ).rejects.toThrow("changed while its outcome was unknown"); - await projection.putItem( - RUN_ID, - "message/started", - { type: "provider", providerEventId: "message-1" }, - item, - ); - await write(); - - expect(mutationIds).toEqual([mutationIds[0], mutationIds[0]]); - expect(projection.item(RUN_ID, item.id)).toEqual(item); -}); - -test("Contract projection retries a derived terminal intent without changing it", async () => { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - const writes: ContractAuthorityUpdate[] = []; - const projection = new ContractProjection({ - authority: async (update) => { - writes.push(update); - - if (writes.length === 1) { - throw new AuthorityOutcomeUnknownError("response lost after commit"); - } - }, - now: () => new Date(nowMs), - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(new Date(nowMs).toISOString())); - const finish = () => - projection.finishRun({ - cause: { providerEventId: "turn-1", type: "provider" }, - event: "turn/completed", - runId: RUN_ID, - status: "completed", - }); - - await expect(finish()).rejects.toThrow("response lost"); - nowMs += 1; - await finish(); - - expect(writes.map((write) => write.mutationId)).toEqual([ - writes[0]?.mutationId, - writes[0]?.mutationId, - ]); - expect(writes[1]?.operations).toEqual(writes[0]?.operations); -}); - -test("Contract projection retries a checkpoint with its first timestamp", async () => { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - const writes: ContractAuthorityUpdate[] = []; - const projection = new ContractProjection({ - authority: async (update) => { - if (update.event === "message/delta.checkpoint") { - writes.push(update); - - if (writes.length === 1) { - throw new AuthorityOutcomeUnknownError("response lost after commit"); - } - } - }, - now: () => new Date(nowMs), - preview: () => {}, - previewCheckpointBytes: 1, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(new Date(nowMs).toISOString())); - await projection.putItem( - RUN_ID, - "message/started", - { name: "start", type: "system" }, - activeMessage("message-1", new Date(nowMs).toISOString()), - ); - const append = () => - projection.appendText({ - cause: { providerEventId: "delta-1", type: "provider" }, - channel: "message.text", - delta: "x", - event: "message/delta", - itemId: "message-1", - runId: RUN_ID, - }); - - await expect(append()).rejects.toThrow("response lost"); - nowMs += 1; - await append(); - - expect(writes[1]?.mutationId).toBe(writes[0]?.mutationId); - expect(writes[1]?.operations).toEqual(writes[0]?.operations); - expect(projection.item(RUN_ID, "message-1")).toMatchObject({ - content: [{ text: "x", type: "text" }], - }); -}); - -test("Contract projection serializes Authority writes and local state", async () => { - const firstEntered = Promise.withResolvers(); - const releaseFirst = Promise.withResolvers(); - const events: string[] = []; - const projection = new ContractProjection({ - authority: async ({ event }) => { - events.push(event); - if (event === "first") { - firstEntered.resolve(); - await releaseFirst.promise; - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - const timestamp = "2026-07-16T08:00:00.000Z"; - projection.attachRun(activeRun(timestamp)); - const first = projection.putItem( - RUN_ID, - "first", - { type: "system", name: "first" }, - { ...activeMessage("message-1", timestamp), content: [{ text: "first", type: "text" }] }, - ); - await firstEntered.promise; - const second = projection.putItem( - RUN_ID, - "second", - { type: "system", name: "second" }, - { ...activeMessage("message-1", timestamp), content: [{ text: "second", type: "text" }] }, - ); - await Promise.resolve(); - - expect(events).toEqual(["first"]); - releaseFirst.resolve(); - await Promise.all([first, second]); - - expect(events).toEqual(["first", "second"]); - expect(projection.item(RUN_ID, "message-1")).toMatchObject({ - content: [{ text: "second", type: "text" }], - }); -}); - -test.each([ - ["a queued checkpoint", "b", "cd", ["ab", "abcd"]], - ["a queued Preview append", "bc", "d", ["abc"]], -] as const)( - "Contract projection preserves text across %s", - async (_name, firstDelta, followerDelta, expectedCheckpoints) => { - const checkpointEntered = Promise.withResolvers(); - const releaseCheckpoint = Promise.withResolvers(); - const checkpoints: string[] = []; - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async ({ event, operations }) => { - if (!event.endsWith(".checkpoint")) { - return; - } - - const operation = operations[0]; - if (operation?.op === "put" && operation.entity === "item") { - checkpoints.push( - operation.value.kind === "message" - ? operation.value.content - .flatMap((block) => (block.type === "text" ? [block.text] : [])) - .join("") - : "", - ); - } - - if (checkpoints.length === 1) { - checkpointEntered.resolve(); - await releaseCheckpoint.promise; - } - }, - now: () => new Date(timestamp), - preview: () => {}, - previewCheckpointBytes: 2, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - await projection.putItem( - RUN_ID, - "message/started", - { name: "start", type: "system" }, - activeMessage("message-1", timestamp), - ); - const append = (delta: string, event: string) => - projection.appendText({ - cause: { providerEventId: event, type: "provider" }, - channel: "message.text", - delta, - event, - itemId: "message-1", - runId: RUN_ID, - }); - await append("a", "delta-a"); - const first = append(firstDelta, "delta-first"); - await checkpointEntered.promise; - const follower = append(followerDelta, "delta-follower"); - releaseCheckpoint.resolve(); - await Promise.all([first, follower]); - - expect(checkpoints).toEqual(expectedCheckpoints); - expect(projection.materializedText(RUN_ID, "message-1", "message.text")).toBe( - `a${firstDelta}${followerDelta}`, - ); - }, -); diff --git a/tests/contract-projection-preview.test.ts b/tests/contract-projection-preview.test.ts deleted file mode 100644 index 907a879..0000000 --- a/tests/contract-projection-preview.test.ts +++ /dev/null @@ -1,662 +0,0 @@ -import { expect, test } from "bun:test"; - -import { AuthorityOutcomeUnknownError, interactionSchema, itemSchema } from "../src/contract"; -import type { Item, Run } from "../src/contract"; -import { - ContractProjection, - type ContractAuthorityUpdate, -} from "../src/runtimes/contract-projection"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const INTERACTION_ID = protocolId(3); - -type FinishRunInput = Parameters[0]; - -function activeRun(startedAt: string, input = true): Run { - return { - id: RUN_ID, - input: input ? [{ text: "hello", type: "text" }] : [], - origin: input ? "user" : "system", - startedAt, - status: "active", - }; -} - -function activeMessage(id: string, timestamp: string): Extract { - return itemSchema.parse({ - audience: "participants", - content: [], - createdAt: timestamp, - id, - kind: "message", - role: "agent", - runId: RUN_ID, - status: "active", - updatedAt: timestamp, - }) as Extract; -} - -test("Contract projection keeps Preview appended while checkpointText awaits Authority", async () => { - const checkpointEntered = Promise.withResolvers(); - const releaseCheckpoint = Promise.withResolvers(); - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async ({ event }) => { - if (event === "message/checkpoint") { - checkpointEntered.resolve(); - await releaseCheckpoint.promise; - } - }, - now: () => new Date(timestamp), - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - await projection.putItem( - RUN_ID, - "message/started", - { name: "start", type: "system" }, - activeMessage("message-1", timestamp), - ); - const append = (delta: string, event: string) => - projection.appendText({ - cause: { providerEventId: event, type: "provider" }, - channel: "message.text", - delta, - event, - itemId: "message-1", - runId: RUN_ID, - }); - await append("a", "delta-a"); - const checkpoint = projection.checkpointText({ - cause: { providerEventId: "checkpoint-a", type: "provider" }, - channel: "message.text", - event: "message/checkpoint", - itemId: "message-1", - runId: RUN_ID, - }); - await checkpointEntered.promise; - let appendSettled = false; - const follower = append("b", "delta-b").finally(() => { - appendSettled = true; - }); - await Promise.resolve(); - const settledBeforeCheckpoint = appendSettled; - releaseCheckpoint.resolve(); - await Promise.all([checkpoint, follower]); - - expect(settledBeforeCheckpoint).toBe(false); - expect(projection.materializedText(RUN_ID, "message-1", "message.text")).toBe("ab"); -}); - -test("Contract projection rejects Preview followers until an unknown checkpoint is retried", async () => { - const checkpointEntered = Promise.withResolvers(); - const rejectCheckpoint = Promise.withResolvers(); - const timestamp = "2026-07-16T08:00:00.000Z"; - let attempts = 0; - const projection = new ContractProjection({ - authority: async ({ event }) => { - if (event === "message/checkpoint" && ++attempts === 1) { - checkpointEntered.resolve(); - await rejectCheckpoint.promise; - throw new AuthorityOutcomeUnknownError("checkpoint result lost"); - } - }, - now: () => new Date(timestamp), - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - await projection.putItem( - RUN_ID, - "message/started", - { name: "start", type: "system" }, - activeMessage("message-1", timestamp), - ); - const append = (delta: string, event: string) => - projection.appendText({ - cause: { providerEventId: event, type: "provider" }, - channel: "message.text", - delta, - event, - itemId: "message-1", - runId: RUN_ID, - }); - const checkpoint = () => - projection.checkpointText({ - cause: { providerEventId: "checkpoint-a", type: "provider" }, - channel: "message.text", - event: "message/checkpoint", - itemId: "message-1", - runId: RUN_ID, - }); - await append("a", "delta-a"); - const first = checkpoint(); - await checkpointEntered.promise; - const follower = append("b", "delta-b"); - const results = Promise.allSettled([first, follower]); - rejectCheckpoint.resolve(); - - const [checkpointResult, followerResult] = await results; - expect(checkpointResult).toMatchObject({ - reason: { message: "checkpoint result lost" }, - status: "rejected", - }); - expect(followerResult).toMatchObject({ status: "rejected" }); - expect(followerResult.status === "rejected" ? followerResult.reason : undefined).toBeInstanceOf( - AuthorityOutcomeUnknownError, - ); - await checkpoint(); - await append("b", "delta-b"); - - expect(projection.materializedText(RUN_ID, "message-1", "message.text")).toBe("ab"); -}); - -test("Contract projection derives finishRun after an in-flight Item update", async () => { - const updateEntered = Promise.withResolvers(); - const releaseUpdate = Promise.withResolvers(); - const writes: ContractAuthorityUpdate[] = []; - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async (update) => { - writes.push(update); - if (update.event === "message/updated") { - updateEntered.resolve(); - await releaseUpdate.promise; - } - }, - now: () => new Date(timestamp), - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - await projection.putItem( - RUN_ID, - "message/started", - { name: "start", type: "system" }, - activeMessage("message-1", timestamp), - ); - const updated = projection.putItem( - RUN_ID, - "message/updated", - { name: "update", type: "system" }, - { - ...activeMessage("message-1", timestamp), - content: [{ text: "latest", type: "text" }], - }, - ); - await updateEntered.promise; - const finished = projection.finishRun({ - cause: { name: "finish", type: "system" }, - event: "run/finished", - runId: RUN_ID, - status: "completed", - }); - releaseUpdate.resolve(); - await Promise.all([updated, finished]); - - const finishItem = writes - .find((write) => write.event === "run/finished") - ?.operations.find((operation) => operation.op === "put" && operation.entity === "item"); - expect(finishItem?.value).toMatchObject({ - content: [{ text: "latest", type: "text" }], - status: "completed", - }); -}); - -test("Contract projection derives finishRun after an in-flight Interaction", async () => { - const interactionEntered = Promise.withResolvers(); - const releaseInteraction = Promise.withResolvers(); - const writes: ContractAuthorityUpdate[] = []; - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async (update) => { - writes.push(update); - if (update.event === "permission/requested") { - interactionEntered.resolve(); - await releaseInteraction.promise; - } - }, - now: () => new Date(timestamp), - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - const opened = projection.putInteraction( - RUN_ID, - "permission/requested", - { providerEventId: "permission-1", type: "provider" }, - interactionSchema.parse({ - audience: "participants", - blocking: true, - createdAt: timestamp, - expiresAt: "2026-07-16T08:05:00.000Z", - id: INTERACTION_ID, - kind: "permission", - request: { - options: [{ effect: "deny", id: "deny", label: "Deny", scope: "once" }], - subject: { operation: "execute", targets: ["workspace"], type: "resource" }, - title: "Run command?", - }, - runId: RUN_ID, - status: "open", - }), - ); - await interactionEntered.promise; - const finished = projection.finishRun({ - cause: { name: "finish", type: "system" }, - event: "run/finished", - runId: RUN_ID, - status: "completed", - }); - releaseInteraction.resolve(); - await Promise.all([opened, finished]); - - expect( - writes - .find((write) => write.event === "run/finished") - ?.operations.find((operation) => operation.op === "put" && operation.entity === "interaction") - ?.value, - ).toMatchObject({ id: INTERACTION_ID, status: "expired" }); -}); - -test("Contract projection derives finishRun after an in-flight text checkpoint", async () => { - const checkpointEntered = Promise.withResolvers(); - const releaseCheckpoint = Promise.withResolvers(); - const writes: ContractAuthorityUpdate[] = []; - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async (update) => { - writes.push(update); - if (update.event === "message/delta.checkpoint") { - checkpointEntered.resolve(); - await releaseCheckpoint.promise; - } - }, - now: () => new Date(timestamp), - preview: () => {}, - previewCheckpointBytes: 2, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - await projection.putItem( - RUN_ID, - "message/started", - { name: "start", type: "system" }, - activeMessage("message-1", timestamp), - ); - const append = (delta: string) => - projection.appendText({ - cause: { providerEventId: `delta-${delta}`, type: "provider" }, - channel: "message.text", - delta, - event: "message/delta", - itemId: "message-1", - runId: RUN_ID, - }); - await append("a"); - const checkpoint = append("b"); - await checkpointEntered.promise; - const finished = projection.finishRun({ - cause: { name: "finish", type: "system" }, - event: "run/finished", - runId: RUN_ID, - status: "completed", - }); - releaseCheckpoint.resolve(); - await Promise.all([checkpoint, finished]); - - const finishItem = writes - .find((write) => write.event === "run/finished") - ?.operations.find((operation) => operation.op === "put" && operation.entity === "item"); - expect(finishItem?.value).toMatchObject({ status: "completed" }); - expect( - finishItem?.value.kind === "message" - ? finishItem.value.content - .flatMap((block) => (block.type === "text" ? [block.text] : [])) - .join("") - : undefined, - ).toBe("ab"); -}); - -test.each([ - ["status", { status: "completed" }, { status: "failed" }], - [ - "error", - { - error: { code: "first", message: "first", retryable: false }, - status: "failed", - }, - { - error: { code: "second", message: "second", retryable: false }, - status: "failed", - }, - ], - [ - "terminalItems", - { status: "completed", terminalItems: [] }, - { - status: "completed", - terminalItems: [ - itemSchema.parse({ - ...activeMessage("message-1", "2026-07-16T08:00:00.000Z"), - endedAt: "2026-07-16T08:00:01.000Z", - status: "completed", - updatedAt: "2026-07-16T08:00:01.000Z", - }), - ], - }, - ], - [ - "activeItemStatus", - { activeItemStatus: "completed", status: "completed" }, - { activeItemStatus: "cancelled", status: "completed" }, - ], -] satisfies readonly [string, Partial, Partial][])( - "Contract projection rejects changed finishRun %s after an unknown result", - async (_name, firstPatch, changedPatch) => { - const writes: ContractAuthorityUpdate[] = []; - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async (update) => { - writes.push(update); - if (writes.length === 1) { - throw new AuthorityOutcomeUnknownError("finish result lost"); - } - }, - now: () => new Date(timestamp), - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - const base = { - cause: { providerEventId: "finish-1", type: "provider" as const }, - event: "run/finished", - runId: RUN_ID, - }; - const first = { ...base, ...firstPatch } as FinishRunInput; - const changed = { ...base, ...changedPatch } as FinishRunInput; - - await expect(projection.finishRun(first)).rejects.toThrow("finish result lost"); - await expect(projection.finishRun(changed)).rejects.toThrow( - "changed while its outcome was unknown", - ); - await projection.finishRun(first); - - expect(writes).toHaveLength(2); - expect(writes[1]?.mutationId).toBe(writes[0]?.mutationId); - expect(writes[1]?.operations).toEqual(writes[0]?.operations); - }, -); - -test("Contract projection fail-stops writes behind an unresolved Authority outcome", async () => { - const firstEntered = Promise.withResolvers(); - const rejectFirst = Promise.withResolvers(); - const events: string[] = []; - let firstAttempts = 0; - const projection = new ContractProjection({ - authority: async ({ event }) => { - events.push(event); - if (event === "first" && ++firstAttempts === 1) { - firstEntered.resolve(); - await rejectFirst.promise; - throw new AuthorityOutcomeUnknownError("response lost after commit"); - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - const timestamp = "2026-07-16T08:00:00.000Z"; - projection.attachRun(activeRun(timestamp)); - const firstItem = { - ...activeMessage("message-1", timestamp), - content: [{ text: "first", type: "text" as const }], - }; - const secondItem = { - ...activeMessage("message-1", timestamp), - content: [{ text: "second", type: "text" as const }], - }; - const first = projection.putItem(RUN_ID, "first", { name: "first", type: "system" }, firstItem); - await firstEntered.promise; - const second = projection.putItem( - RUN_ID, - "second", - { name: "second", type: "system" }, - secondItem, - ); - const failures = Promise.allSettled([first, second]); - rejectFirst.resolve(); - - expect(await failures).toMatchObject([ - { reason: { message: "response lost after commit" }, status: "rejected" }, - { reason: { message: "response lost after commit" }, status: "rejected" }, - ]); - await expect( - projection.putItem( - RUN_ID, - "second", - { name: "second", type: "system" }, - { ...secondItem, content: [{ text: "changed", type: "text" }] }, - ), - ).rejects.toBeInstanceOf(AuthorityOutcomeUnknownError); - expect(events).toEqual(["first"]); - - await projection.putItem(RUN_ID, "first", { name: "first", type: "system" }, firstItem); - await projection.putItem(RUN_ID, "second", { name: "second", type: "system" }, secondItem); - - expect(events).toEqual(["first", "first", "second"]); - expect(projection.item(RUN_ID, "message-1")).toEqual(secondItem); -}); - -test("Contract projection lets an exact retry pass queued followers", async () => { - const firstEntered = Promise.withResolvers(); - const rejectFirst = Promise.withResolvers(); - const events: string[] = []; - let attempts = 0; - const projection = new ContractProjection({ - authority: async ({ event }) => { - events.push(event); - if (event === "first" && ++attempts === 1) { - firstEntered.resolve(); - await rejectFirst.promise; - throw new AuthorityOutcomeUnknownError("result lost"); - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - const timestamp = "2026-07-16T08:00:00.000Z"; - const firstItem = activeMessage("message-1", timestamp); - const secondItem = activeMessage("message-2", timestamp); - projection.attachRun(activeRun(timestamp)); - const first = projection.putItem(RUN_ID, "first", { name: "first", type: "system" }, firstItem); - await firstEntered.promise; - const follower = projection.putItem( - RUN_ID, - "second", - { name: "second", type: "system" }, - secondItem, - ); - const retry = first.catch(() => - projection.putItem(RUN_ID, "first", { name: "first", type: "system" }, firstItem), - ); - const results = Promise.allSettled([first, follower, retry]); - rejectFirst.resolve(); - - expect(await results).toMatchObject([ - { status: "rejected" }, - { status: "rejected" }, - { status: "fulfilled" }, - ]); - expect(events).toEqual(["first", "first"]); - await projection.putItem(RUN_ID, "second", { name: "second", type: "system" }, secondItem); - expect(events).toEqual(["first", "first", "second"]); -}); - -test("Contract projection rejects an identical follower queued before an unknown result", async () => { - const firstEntered = Promise.withResolvers(); - const rejectFirst = Promise.withResolvers(); - const mutationIds: string[] = []; - let attempts = 0; - const projection = new ContractProjection({ - authority: async ({ mutationId }) => { - mutationIds.push(mutationId); - if (++attempts === 1) { - firstEntered.resolve(); - await rejectFirst.promise; - throw new AuthorityOutcomeUnknownError("result lost"); - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - const timestamp = "2026-07-16T08:00:00.000Z"; - const item = activeMessage("message-1", timestamp); - projection.attachRun(activeRun(timestamp)); - const write = () => projection.putItem(RUN_ID, "first", { name: "first", type: "system" }, item); - const first = write(); - await firstEntered.promise; - const follower = write(); - const results = Promise.allSettled([first, follower]); - rejectFirst.resolve(); - - expect(await results).toMatchObject([{ status: "rejected" }, { status: "rejected" }]); - expect(attempts).toBe(1); - await write(); - - expect(attempts).toBe(2); - expect(mutationIds[1]).toBe(mutationIds[0]); -}); - -test("Contract projection continues queued writes after a definite Authority rejection", async () => { - const firstEntered = Promise.withResolvers(); - const rejectFirst = Promise.withResolvers(); - const events: string[] = []; - const projection = new ContractProjection({ - authority: async ({ event }) => { - events.push(event); - if (event === "first") { - firstEntered.resolve(); - await rejectFirst.promise; - throw new Error("mutation rejected"); - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - const timestamp = "2026-07-16T08:00:00.000Z"; - projection.attachRun(activeRun(timestamp)); - const first = projection.putItem( - RUN_ID, - "first", - { name: "first", type: "system" }, - activeMessage("message-1", timestamp), - ); - await firstEntered.promise; - const secondItem = { - ...activeMessage("message-1", timestamp), - content: [{ text: "second", type: "text" as const }], - }; - const second = projection.putItem( - RUN_ID, - "second", - { name: "second", type: "system" }, - secondItem, - ); - const results = Promise.allSettled([first, second]); - rejectFirst.resolve(); - - expect(await results).toMatchObject([ - { reason: { message: "mutation rejected" }, status: "rejected" }, - { status: "fulfilled" }, - ]); - expect(events).toEqual(["first", "second"]); - expect(projection.item(RUN_ID, "message-1")).toEqual(secondItem); -}); - -test("Contract projection preserves sub-millisecond lifecycle order", async () => { - let terminalRun: Run | undefined; - const projection = new ContractProjection({ - authority: async ({ operations }) => { - const operation = operations.at(-1); - terminalRun = - operation?.op === "put" && operation.entity === "run" ? operation.value : undefined; - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun("2026-07-16T08:00:00.0000009Z")); - - await projection.finishRun({ - cause: { type: "system", name: "finish" }, - endedAt: "2026-07-16T08:00:00.0000001Z", - event: "finish", - runId: RUN_ID, - status: "completed", - }); - - expect(terminalRun).toMatchObject({ endedAt: "2026-07-16T08:00:00.0000009Z" }); -}); - -test("Contract projection applies Authority admission before dispatch", async () => { - let calls = 0; - const projection = new ContractProjection({ - admissionLimits: { maxBytes: 1_024, maxInlineBytes: 1 }, - authority: async () => { - calls += 1; - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - const timestamp = "2026-07-16T08:00:00.000Z"; - projection.attachRun(activeRun(timestamp)); - - await expect( - projection.putItem( - RUN_ID, - "image", - { type: "system", name: "image" }, - { - ...activeMessage("message-1", timestamp), - content: [{ data: "aGk=", mediaType: "text/plain", type: "inline_blob" }], - }, - ), - ).rejects.toThrow("inline Blob"); - expect(calls).toBe(0); - - const item = activeMessage("message-1", timestamp); - await expect( - projection.putItem(RUN_ID, "image", { name: "image", type: "system" }, item), - ).resolves.toEqual(item); - expect(calls).toBe(1); -}); - -test.each([ - ["an identical retry", (run: Run) => structuredClone(run), null], - [ - "changed state", - (run: Run) => ({ ...run, input: [{ text: "changed", type: "text" as const }] }), - "already attached with different state", - ], -] as const)("Contract projection handles %s for an attached Run", (_name, retry, error) => { - const projection = new ContractProjection({ - authority: async () => {}, - preview: () => {}, - sessionId: SESSION_ID, - }); - const run = activeRun("2026-07-16T08:00:00.000Z"); - projection.attachRun(run); - - if (error === null) { - expect(() => projection.attachRun(retry(run))).not.toThrow(); - expect(projection.run(RUN_ID)).toEqual(run); - } else { - expect(() => projection.attachRun(retry(run))).toThrow(error); - expect(projection.run(RUN_ID)).toEqual(run); - } -}); diff --git a/tests/contract-projection-state.test.ts b/tests/contract-projection-state.test.ts deleted file mode 100644 index 884e5a4..0000000 --- a/tests/contract-projection-state.test.ts +++ /dev/null @@ -1,845 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - itemSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - Item, - Run, - SessionSnapshot, -} from "../src/contract"; -import { - ContractProjection, - type ContractAuthorityUpdate, -} from "../src/runtimes/contract-projection"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const INTERACTION_ID = protocolId(3); -const RESOLVED_INTERACTION_ID = protocolId(5); - -function activeRun(startedAt: string, input = true): Run { - return { - id: RUN_ID, - input: input ? [{ text: "hello", type: "text" }] : [], - origin: input ? "user" : "system", - startedAt, - status: "active", - }; -} - -function childRun(id: string, parentRunId: string, startedAt: string): Run { - return { - id, - input: [], - origin: "system", - parentRunId, - startedAt, - status: "active", - }; -} - -function runTreeProjection(runs: readonly Run[], capturedAt: string) { - const root = runs[0]; - - if (root === undefined) { - throw new Error("Run tree fixture requires a root."); - } - - let committedAt = capturedAt; - let snapshot: SessionSnapshot = validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs, - session: { - capabilities: { "run.child": {} }, - config: [], - createdAt: root.startedAt, - id: SESSION_ID, - status: "open", - updatedAt: root.startedAt, - }, - }); - const projection = new ContractProjection({ - authority: async (update) => { - const revision = snapshot.revision + 1; - snapshot = applyCommittedMutation(snapshot, { - baseRevision: snapshot.revision, - cause: update.cause, - committedAt, - mutationId: protocolId(2_000 + revision), - operations: [...update.operations], - revision, - sessionId: SESSION_ID, - }); - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - - for (const run of runs) { - projection.attachRun(run); - } - - return { - projection, - setCommittedAt(value: string) { - committedAt = value; - }, - snapshot: () => snapshot, - }; -} - -function activeMessage(id: string, timestamp: string): Extract { - return itemSchema.parse({ - audience: "participants", - content: [], - createdAt: timestamp, - id, - kind: "message", - role: "agent", - runId: RUN_ID, - status: "active", - updatedAt: timestamp, - }) as Extract; -} - -test("Contract projection commits terminal snapshots and remaining cleanup atomically", async () => { - const startedAt = "2026-07-16T08:00:00.000Z"; - const endedAt = "2026-07-16T08:00:01.000Z"; - const commits: AuthorityOperation[][] = []; - const projection = new ContractProjection({ - authority: async ({ operations }) => { - commits.push([...operations]); - }, - now: () => new Date(endedAt), - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(startedAt)); - await projection.putItem( - RUN_ID, - "message/started", - { providerEventId: "message-1", type: "provider" }, - activeMessage("message-1", startedAt), - ); - await projection.putItem( - RUN_ID, - "message/started", - { providerEventId: "message-2", type: "provider" }, - activeMessage("message-2", startedAt), - ); - const error = { code: "turn.incomplete", message: "Missing snapshot.", retryable: false }; - - await projection.finishRun({ - cause: { providerEventId: "turn/completed", type: "provider" }, - error, - event: "turn/completed", - runId: RUN_ID, - status: "failed", - terminalItems: [ - itemSchema.parse({ - ...activeMessage("message-1", startedAt), - content: [{ text: "done", type: "text" }], - endedAt, - status: "completed", - updatedAt: endedAt, - }), - ], - }); - - expect(commits.at(-1)).toMatchObject([ - { entity: "item", op: "put", value: { id: "message-1", status: "completed" } }, - { - entity: "item", - op: "put", - value: { error, id: "message-2", status: "failed" }, - }, - { entity: "run", op: "put", value: { error, id: RUN_ID, status: "failed" } }, - ]); - expect(projection.run(RUN_ID)).toBeUndefined(); -}); - -test.each([ - [ - "an older requested end", - "2026-07-16T08:00:01.000Z", - "2026-07-16T08:00:00.500Z", - "2026-07-16T08:00:01.000Z", - ], - [ - "a newer requested end", - "2026-07-16T08:00:01.000Z", - "2026-07-16T08:00:02.000Z", - "2026-07-16T08:00:02.000Z", - ], - [ - "an offset Item update", - "2026-07-16T16:00:01.500+08:00", - "2026-07-16T09:00:01.000+01:00", - "2026-07-16T16:00:01.500+08:00", - ], - [ - "a sub-millisecond Item update", - "2026-07-16T08:00:00.1000009Z", - "2026-07-16T08:00:00.1000001Z", - "2026-07-16T08:00:00.1000009Z", - ], -] as const)( - "Contract projection ends an atomic terminal Item enrichment after %s", - async (_name, updatedAt, requestedEnd, expectedEnd) => { - const startedAt = "2026-07-16T08:00:00.000Z"; - const itemEndedAt = "2026-07-16T08:00:00.100Z"; - let committedAt = itemEndedAt; - let snapshot: SessionSnapshot = validateSessionSnapshot({ - capturedAt: startedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(startedAt)], - session: { - capabilities: {}, - config: [], - createdAt: startedAt, - id: SESSION_ID, - status: "open", - updatedAt: startedAt, - }, - }); - const projection = new ContractProjection({ - authority: async (update) => { - const revision = snapshot.revision + 1; - snapshot = applyCommittedMutation(snapshot, { - baseRevision: snapshot.revision, - cause: update.cause, - committedAt, - mutationId: protocolId(1_000 + revision), - operations: [...update.operations], - revision, - sessionId: SESSION_ID, - }); - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(startedAt)); - const terminal = itemSchema.parse({ - ...activeMessage("message-1", startedAt), - content: [{ text: "before", type: "text" }], - endedAt: itemEndedAt, - status: "completed", - updatedAt: itemEndedAt, - }); - await projection.putItem( - RUN_ID, - "message/completed", - { providerEventId: "message-1", type: "provider" }, - terminal, - ); - committedAt = "2026-07-18T00:00:00.000Z"; - - await projection.finishRun({ - cause: { providerEventId: "run-1", type: "provider" }, - endedAt: requestedEnd, - event: "run/completed", - runId: RUN_ID, - status: "completed", - terminalItems: [ - itemSchema.parse({ - ...terminal, - content: [{ text: "after", type: "text" }], - updatedAt, - }), - ], - }); - - expect(snapshot.items[0]).toMatchObject({ - content: [{ text: "after", type: "text" }], - endedAt: itemEndedAt, - updatedAt, - }); - expect(snapshot.runs[0]).toMatchObject({ endedAt: expectedEnd, status: "completed" }); - }, -); - -test("Contract projection bubbles a descendant Run boundary through every parent", async () => { - const startedAt = "2026-07-16T08:00:00.000Z"; - const childRunId = protocolId(7); - const grandchildRunId = protocolId(8); - const child = childRun(childRunId, RUN_ID, "2026-07-16T08:00:00.100Z"); - const grandchild = childRun(grandchildRunId, childRunId, "2026-07-16T08:00:00.200Z"); - const descendantEnd = "2026-07-16T16:00:03.0000009+08:00"; - const tree = runTreeProjection([activeRun(startedAt), child, grandchild], grandchild.startedAt); - - tree.setCommittedAt(descendantEnd); - await tree.projection.finishRun({ - cause: { providerEventId: "grandchild-1", type: "provider" }, - endedAt: descendantEnd, - event: "grandchild/completed", - runId: grandchildRunId, - status: "completed", - }); - tree.setCommittedAt("2026-07-16T08:00:04.000Z"); - await tree.projection.finishRun({ - cause: { providerEventId: "child-1", type: "provider" }, - endedAt: "2026-07-16T08:00:01.000Z", - event: "child/completed", - runId: childRunId, - status: "completed", - }); - tree.setCommittedAt("2026-07-16T08:00:05.000Z"); - await tree.projection.finishRun({ - cause: { providerEventId: "parent-1", type: "provider" }, - endedAt: "2026-07-16T08:00:02.000Z", - event: "parent/completed", - runId: RUN_ID, - status: "completed", - }); - - expect(tree.snapshot().runs).toEqual( - expect.arrayContaining([ - expect.objectContaining({ endedAt: descendantEnd, id: childRunId }), - expect.objectContaining({ endedAt: descendantEnd, id: RUN_ID }), - ]), - ); -}); - -test("Contract projection retains the latest sibling Run boundary regardless of finish order", async () => { - const startedAt = "2026-07-16T08:00:00.000Z"; - const firstRunId = protocolId(7); - const secondRunId = protocolId(8); - const first = childRun(firstRunId, RUN_ID, "2026-07-16T08:00:00.100Z"); - const second = childRun(secondRunId, RUN_ID, "2026-07-16T08:00:00.200Z"); - const latestEnd = "2026-07-16T16:00:04.0000009+08:00"; - const tree = runTreeProjection([activeRun(startedAt), first, second], second.startedAt); - - tree.setCommittedAt("2026-07-16T08:00:05.000Z"); - await tree.projection.finishRun({ - cause: { providerEventId: "first-1", type: "provider" }, - endedAt: latestEnd, - event: "first/completed", - runId: firstRunId, - status: "completed", - }); - tree.setCommittedAt("2026-07-16T08:00:06.000Z"); - await tree.projection.finishRun({ - cause: { providerEventId: "second-1", type: "provider" }, - endedAt: "2026-07-16T08:00:03.000Z", - event: "second/completed", - runId: secondRunId, - status: "completed", - }); - tree.setCommittedAt("2026-07-16T08:00:07.000Z"); - await tree.projection.finishRun({ - cause: { providerEventId: "parent-1", type: "provider" }, - endedAt: "2026-07-16T08:00:02.000Z", - event: "parent/completed", - runId: RUN_ID, - status: "completed", - }); - - expect(tree.snapshot().runs.find((run) => run.id === RUN_ID)).toMatchObject({ - endedAt: latestEnd, - status: "completed", - }); -}); - -test.each([ - ["an earlier requested end", "2026-07-16T08:00:01.0000001Z", "child"], - ["a later requested end", "2026-07-16T08:00:02.0000001Z", "requested"], -] as const)( - "Contract projection combines a finished child Run with %s", - async (_name, requestedEnd, expected) => { - const startedAt = "2026-07-16T08:00:00.000Z"; - const childStartedAt = "2026-07-16T08:00:00.500Z"; - const childEndedAt = "2026-07-16T16:00:01.0000009+08:00"; - const childRunId = protocolId(7); - const child = childRun(childRunId, RUN_ID, childStartedAt); - const tree = runTreeProjection([activeRun(startedAt), child], childStartedAt); - const { projection } = tree; - - tree.setCommittedAt(childEndedAt); - await projection.finishRun({ - cause: { providerEventId: "child-1", type: "provider" }, - endedAt: childEndedAt, - event: "child/completed", - runId: childRunId, - status: "completed", - }); - tree.setCommittedAt("2026-07-18T00:00:00.000Z"); - await projection.finishRun({ - cause: { providerEventId: "parent-1", type: "provider" }, - endedAt: requestedEnd, - event: "parent/completed", - runId: RUN_ID, - status: "completed", - }); - - expect(tree.snapshot().runs.find((run) => run.id === RUN_ID)).toMatchObject({ - endedAt: expected === "child" ? childEndedAt : requestedEnd, - status: "completed", - }); - }, -); - -test("Contract projection propagates a child boundary once after an unknown Authority retry", async () => { - const startedAt = "2026-07-16T08:00:00.000Z"; - const childStartedAt = "2026-07-16T08:00:00.500Z"; - const childEndedAt = "2026-07-16T08:00:01.0000009Z"; - const childRunId = protocolId(7); - const child = childRun(childRunId, RUN_ID, childStartedAt); - let committedAt = childEndedAt; - let childAttempts = 0; - let snapshot: SessionSnapshot = validateSessionSnapshot({ - capturedAt: childStartedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(startedAt), child], - session: { - capabilities: { "run.child": {} }, - config: [], - createdAt: startedAt, - id: SESSION_ID, - status: "open", - updatedAt: startedAt, - }, - }); - const committed = new Set(); - const writes: ContractAuthorityUpdate[] = []; - const projection = new ContractProjection({ - authority: async (update) => { - writes.push(update); - - if (!committed.has(update.mutationId)) { - const revision = snapshot.revision + 1; - snapshot = applyCommittedMutation(snapshot, { - baseRevision: snapshot.revision, - cause: update.cause, - committedAt, - mutationId: update.mutationId, - operations: [...update.operations], - revision, - sessionId: SESSION_ID, - }); - committed.add(update.mutationId); - } - - if (update.event === "child/completed" && ++childAttempts === 1) { - throw new AuthorityOutcomeUnknownError("child result lost after commit"); - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(startedAt)); - projection.attachRun(child); - const finishChild = () => - projection.finishRun({ - cause: { providerEventId: "child-1", type: "provider" }, - endedAt: childEndedAt, - event: "child/completed", - runId: childRunId, - status: "completed", - }); - - await expect(finishChild()).rejects.toThrow("child result lost after commit"); - expect(projection.run(childRunId)).toMatchObject({ status: "active" }); - await finishChild(); - committedAt = "2026-07-16T08:00:02.000Z"; - await projection.finishRun({ - cause: { providerEventId: "parent-1", type: "provider" }, - endedAt: "2026-07-16T08:00:01.0000001Z", - event: "parent/completed", - runId: RUN_ID, - status: "completed", - }); - - expect(writes.slice(0, 2).map(({ mutationId }) => mutationId)).toEqual([ - writes[0]?.mutationId, - writes[0]?.mutationId, - ]); - expect(snapshot.revision).toBe(2); - expect(snapshot.runs.find((run) => run.id === RUN_ID)).toMatchObject({ - endedAt: childEndedAt, - status: "completed", - }); -}); - -test("Contract projection releases child boundaries with their parent and on disposal", async () => { - const startedAt = "2026-07-16T08:00:00.000Z"; - const childRunId = protocolId(7); - const child = childRun(childRunId, RUN_ID, "2026-07-16T08:00:00.500Z"); - const childEndedAt = "2026-07-16T08:00:01.000Z"; - const terminalRuns: Run[] = []; - const options = { - authority: async ({ operations }: ContractAuthorityUpdate) => { - const operation = operations.at(-1); - - if (operation?.op === "put" && operation.entity === "run") { - terminalRuns.push(operation.value); - } - }, - preview: () => {}, - sessionId: SESSION_ID, - }; - const projection = new ContractProjection(options); - projection.attachRun(activeRun(startedAt)); - projection.attachRun(child); - await projection.finishRun({ - cause: { providerEventId: "child-1", type: "provider" }, - endedAt: childEndedAt, - event: "child/completed", - runId: childRunId, - status: "completed", - }); - await projection.finishRun({ - cause: { providerEventId: "parent-1", type: "provider" }, - endedAt: "2026-07-16T08:00:00.750Z", - event: "parent/completed", - runId: RUN_ID, - status: "completed", - }); - - projection.attachRun(activeRun(startedAt)); - await projection.finishRun({ - cause: { providerEventId: "reused-1", type: "provider" }, - endedAt: "2026-07-16T08:00:00.750Z", - event: "reused/completed", - runId: RUN_ID, - status: "completed", - }); - expect(terminalRuns.at(-1)).toMatchObject({ endedAt: "2026-07-16T08:00:00.750Z" }); - - const disposable = new ContractProjection(options); - disposable.attachRun(activeRun(startedAt)); - disposable.attachRun(child); - await disposable.finishRun({ - cause: { providerEventId: "disposable-child-1", type: "provider" }, - endedAt: childEndedAt, - event: "disposable-child/completed", - runId: childRunId, - status: "completed", - }); - disposable.dispose(); - disposable.dispose(); - expect(() => disposable.run(RUN_ID)).toThrow("disposed"); -}); - -test.each([ - ["an active Item", (at: string) => [activeMessage("message-1", at)]], - [ - "an Item from another Run", - (at: string) => [ - itemSchema.parse({ - ...activeMessage("message-1", at), - endedAt: at, - runId: protocolId(99), - status: "completed", - }), - ], - ], - [ - "duplicate Item IDs", - (at: string) => { - const item = itemSchema.parse({ - ...activeMessage("message-1", at), - endedAt: at, - status: "completed", - }); - return [item, item]; - }, - ], -] as const)( - "Contract projection rejects terminal snapshots with %s", - async (_name, terminalItems) => { - const timestamp = "2026-07-16T08:00:00.000Z"; - const projection = new ContractProjection({ - authority: async () => {}, - now: () => new Date(timestamp), - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp)); - - await expect( - projection.finishRun({ - cause: { providerEventId: "turn/completed", type: "provider" }, - event: "turn/completed", - runId: RUN_ID, - status: "completed", - terminalItems: terminalItems(timestamp), - }), - ).rejects.toThrow("invalid terminal Item"); - expect(projection.run(RUN_ID)?.status).toBe("active"); - }, -); - -test("Contract projection never ends a Run before a later Interaction", async () => { - let now = new Date("2026-07-16T08:00:00.000Z"); - let snapshot: SessionSnapshot = validateSessionSnapshot({ - capturedAt: now.toISOString(), - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(now.toISOString())], - session: { - capabilities: { - "example.com/interaction": {}, - "interaction.permission": {}, - }, - config: [], - createdAt: now.toISOString(), - id: SESSION_ID, - status: "open", - updatedAt: now.toISOString(), - }, - }); - const projection = new ContractProjection({ - authority: async (update) => { - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause: update.cause, - committedAt: now.toISOString(), - mutationId: protocolId(1_000 + revision), - operations: [...update.operations] as AuthorityOperation[], - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - }, - now: () => now, - preview: () => {}, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(now.toISOString())); - now = new Date("2026-07-16T08:00:01.000Z"); - await projection.putInteraction( - RUN_ID, - "permission/requested", - { providerEventId: "permission-1", type: "provider" }, - interactionSchema.parse({ - audience: "participants", - blocking: true, - createdAt: now.toISOString(), - expiresAt: "2026-07-16T08:05:01.000Z", - id: INTERACTION_ID, - kind: "permission", - request: { - options: [ - { - effect: "deny", - id: "deny", - label: "Deny", - scope: "once", - }, - ], - subject: { - operation: "execute", - targets: ["workspace"], - type: "resource", - }, - title: "Run command?", - }, - runId: RUN_ID, - status: "open", - }), - ); - now = new Date("2026-07-16T08:00:02.000Z"); - await projection.putInteraction( - RUN_ID, - "permission/requested", - { providerEventId: "permission-2-open", type: "provider" }, - interactionSchema.parse({ - audience: "participants", - blocking: true, - createdAt: "2026-07-16T08:00:01.500Z", - expiresAt: "2026-07-16T08:05:01.500Z", - id: RESOLVED_INTERACTION_ID, - kind: "permission", - request: { - options: [ - { - effect: "deny", - id: "deny", - label: "Deny", - scope: "once", - }, - ], - subject: { - operation: "execute", - targets: ["workspace"], - type: "resource", - }, - title: "Run another command?", - }, - runId: RUN_ID, - status: "open", - }), - ); - await projection.putInteraction( - RUN_ID, - "permission/resolved", - { providerEventId: "permission-2-resolved", type: "provider" }, - interactionSchema.parse({ - ...projection.interaction(RESOLVED_INTERACTION_ID), - endedAt: now.toISOString(), - resolution: { type: "cancelled" }, - status: "resolved", - }), - ); - const extensionId = protocolId(6); - await projection.putInteraction( - RUN_ID, - "extension/requested", - { providerEventId: "extension-1", type: "provider" }, - interactionSchema.parse({ - audience: "participants", - blocking: true, - createdAt: now.toISOString(), - expiresAt: "2026-07-16T08:05:02.000Z", - id: extensionId, - kind: "extension", - name: "example.com/interaction", - request: { prompt: "Continue?" }, - runId: RUN_ID, - status: "open", - }), - ); - await projection.finishRun({ - cause: { providerEventId: "result-1", type: "provider" }, - endedAt: "2026-07-16T08:00:00.500Z", - event: "result/completed", - runId: RUN_ID, - status: "completed", - }); - - expect(snapshot.runs[0]).toMatchObject({ - endedAt: "2026-07-16T08:00:02.000Z", - status: "completed", - }); - expect(snapshot.interactions[0]).toMatchObject({ - endedAt: "2026-07-16T08:00:02.000Z", - status: "expired", - }); - expect(snapshot.interactions[0]).not.toHaveProperty("resolution"); - expect(snapshot.interactions.find((entry) => entry.id === extensionId)).toMatchObject({ - endedAt: "2026-07-16T08:00:02.000Z", - status: "expired", - }); -}); - -test("Contract projection checkpoints every text channel before clearing Preview", async () => { - const timestamp = "2026-07-16T08:00:00.000Z"; - const previews: string[] = []; - const projection = new ContractProjection({ - authority: async () => {}, - now: () => new Date(timestamp), - preview: ({ update }) => previews.push(update.text), - previewCheckpointBytes: 4, - sessionId: SESSION_ID, - }); - projection.attachRun(activeRun(timestamp, false)); - await projection.putItem( - RUN_ID, - "terminal/created", - { providerEventId: "terminal-1", type: "provider" }, - itemSchema.parse({ - audience: "participants", - createdAt: timestamp, - id: "terminal-1", - kind: "terminal", - runId: RUN_ID, - status: "active", - stderr: [], - stdout: [], - updatedAt: timestamp, - }), - ); - await projection.replacePreview({ - channel: "terminal.stderr", - itemId: "terminal-1", - runId: RUN_ID, - text: "err", - }); - await projection.replacePreview({ - channel: "terminal.stdout", - itemId: "terminal-1", - runId: RUN_ID, - text: "large", - }); - - expect(projection.item(RUN_ID, "terminal-1")).toMatchObject({ - stderr: [{ text: "err", type: "text" }], - stdout: [{ text: "large", type: "text" }], - }); - expect(projection.materializedText(RUN_ID, "terminal-1", "terminal.stderr")).toBe("err"); - expect(projection.materializedText(RUN_ID, "terminal-1", "terminal.stdout")).toBe("large"); - - await projection.putItem( - RUN_ID, - "message/started", - { providerEventId: "message-1", type: "provider" }, - itemSchema.parse({ - audience: "participants", - content: [], - createdAt: timestamp, - id: "message-1", - kind: "message", - role: "agent", - runId: RUN_ID, - status: "active", - updatedAt: timestamp, - }), - ); - await projection.replacePreview({ - channel: "tool.progress", - itemId: "message-1", - runId: RUN_ID, - text: "a😀b", - }); - - expect(previews.at(-1)).toBe("err"); - await projection.putItem( - RUN_ID, - "tool/started", - { providerEventId: "tool-1", type: "provider" }, - itemSchema.parse({ - audience: "participants", - category: "other", - createdAt: timestamp, - id: "tool-1", - kind: "tool", - name: "Tool", - origin: "provider", - runId: RUN_ID, - status: "active", - updatedAt: timestamp, - }), - ); - await projection.replacePreview({ - channel: "tool.progress", - itemId: "tool-1", - runId: RUN_ID, - text: "a😀b", - }); - - expect(previews.at(-1)).toBe("a"); -}); diff --git a/tests/contract-reducer.test.ts b/tests/contract-reducer.test.ts index 89a727b..ded53f4 100644 --- a/tests/contract-reducer.test.ts +++ b/tests/contract-reducer.test.ts @@ -239,11 +239,13 @@ describe("contract authority reducer", () => { }); test.each([ - ["decreases", { input: 9, total: 10 }], - ["drops a field", { total: 10 }], + ["decreases", { cachedWrite: 10, input: 9, total: 10 }], + ["drops a field", { cachedWrite: 10, total: 10 }], + ["decreases cached writes", { cachedWrite: 9, input: 10, total: 10 }], + ["drops cached writes", { input: 10, total: 10 }], ])("rejects cumulative Run usage that %s", (_name, usage) => { const initial = snapshot(); - const run = { ...activeRun(), usage: { input: 10, total: 10 } }; + const run = { ...activeRun(), usage: { cachedWrite: 10, input: 10, total: 10 } }; const running = applyCommittedMutation( initial, mutation(initial, [{ entity: "run", op: "put", value: run }]), diff --git a/tests/driver-artifact-contract.test.ts b/tests/driver-artifact-contract.test.ts index e4e2fc2..8487b6a 100644 --- a/tests/driver-artifact-contract.test.ts +++ b/tests/driver-artifact-contract.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; -import { OPENAI_APP_SERVER_SCHEMA_VERSION } from "../src/runtimes/openai/generated/app-server-protocol-types"; import { AGENT_DRIVER_VERSION } from "../src/core/version"; type DriverPackageExportTarget = @@ -38,6 +37,26 @@ interface EnvironmentPackageManagerManifest { readonly schemaVersion?: number; } +interface WorkflowStep { + readonly name?: string; + readonly run?: string; + readonly uses?: string; +} + +interface WorkflowJob { + readonly if?: string; + readonly needs?: string | readonly string[]; + readonly steps?: readonly WorkflowStep[]; +} + +interface Workflow { + readonly concurrency?: { + readonly group?: string; + readonly queue?: string; + }; + readonly jobs?: Record; +} + const PUBLIC_EXPORTS = [ ".", "./boot", @@ -65,6 +84,26 @@ function readEnvironmentPackageManagerManifest(): EnvironmentPackageManagerManif ) as EnvironmentPackageManagerManifest; } +function readWorkflow(path: string): Workflow { + return Bun.YAML.parse(readText(path)) as Workflow; +} + +function workflowJob(workflow: Workflow, id: string): WorkflowJob { + const job = workflow.jobs?.[id]; + if (job === undefined) { + throw new Error(`Missing workflow job ${id}.`); + } + return job; +} + +function workflowStep(job: WorkflowJob, name: string): WorkflowStep { + const step = job.steps?.find((candidate) => candidate.name === name); + if (step === undefined) { + throw new Error(`Missing workflow step ${name}.`); + } + return step; +} + describe("driver artifact contract", () => { test("uses the public mosoo package identity", () => { const packageJson = readDriverPackageJson(); @@ -74,8 +113,8 @@ describe("driver artifact contract", () => { expect(packageJson.version).toBe(AGENT_DRIVER_VERSION); expect(packageJson.description).toContain("Agent Driver"); expect(packageJson.license).toBe("Apache-2.0"); - expect(packageJson.packageManager).toBe("bun@1.3.14"); - expect(packageJson.engines).toEqual({ bun: ">=1.3.14" }); + expect(packageJson.packageManager).toBe("bun@1.4.0"); + expect(packageJson.engines).toEqual({ bun: ">=1.4.0" }); expect(packageJson.repository).toEqual({ type: "git", url: "git+https://github.com/langgenius/mosoo-agent-driver.git", @@ -90,7 +129,7 @@ describe("driver artifact contract", () => { "agent-driver": "./dist/driver.mjs", }); expect(packageJson.types).toBe("./dist/types/index.d.ts"); - expect(packageJson.files).toEqual(["dist", "src", "assets"]); + expect(packageJson.files).toEqual(["dist", "src", "!src/runtimes/openai/generated", "assets"]); expect(packageJson.files).not.toContain("tests/fixtures"); }); @@ -145,6 +184,7 @@ describe("driver artifact contract", () => { const packageJson = readDriverPackageJson(); const buildScript = packageJson.scripts?.["build"] ?? ""; const imageBuildScript = packageJson.scripts?.["build:image"] ?? ""; + const imageTestScript = packageJson.scripts?.["test:image:environment"] ?? ""; const containerignore = readText("../.containerignore"); const containerfile = readText("../Containerfile"); const processEntry = readText("../src/bin/driver.ts"); @@ -157,19 +197,27 @@ describe("driver artifact contract", () => { expect(containerfile).toContain("RUN chmod +x /usr/local/bin/agent-driver"); expect(containerfile).toContain("ENV MOSOO_ACP_FALLBACK_COMMAND=opencode"); expect(containerignore).toContain("!dist/driver.mjs"); - expect(imageBuildScript).toBe("vp run build && buildah build -t agent-driver:local ."); + expect(imageBuildScript).toBe( + "vp run build && buildah build --http-proxy=false --platform linux/amd64 -t agent-driver:local .", + ); + expect(imageTestScript).toBe( + "podman run --pull=never --rm --entrypoint node agent-driver:local /usr/local/libexec/mosoo/environment-package-manager-check.mjs smoke", + ); expect(packageJson.scripts?.["prepack"]).toBe("vp run build"); }); - test("pins the OpenAI runtime, SDK, and app-server schema to one stable version", () => { + test("pins the OpenAI runtime, CLI, and app-server schema to one stable version", () => { const packageJson = readDriverPackageJson(); const containerfile = readText("../Containerfile"); + const runtimeVersion = packageJson.devDependencies?.["@openai/codex"]; - expect(packageJson.devDependencies?.["@openai/codex-sdk"]).toBe( - OPENAI_APP_SERVER_SCHEMA_VERSION, + expect(runtimeVersion).toMatch(/^\d+\.\d+\.\d+$/); + expect(containerfile).toContain(`ARG OPENAI_RUNTIME_VERSION=${runtimeVersion}`); + expect(readText("../src/runtimes/openai/generated/README.md")).toContain( + `version \`${runtimeVersion}\``, ); - expect(containerfile).toContain( - `ARG OPENAI_RUNTIME_VERSION=${OPENAI_APP_SERVER_SCHEMA_VERSION}`, + expect(readText("../src/runtimes/openai/generated-json-schema/README.md")).toContain( + `@openai/codex@${runtimeVersion}`, ); }); @@ -233,23 +281,93 @@ describe("driver artifact contract", () => { expect(liveTest).not.toMatch(/from ["']\.\.\/src\/(?:core|runtimes)/); }); - test("pins the release OpenCode executable and gates publishing on the packed artifact", () => { + test("pins runtime executables and keeps release publication monotonic", () => { const packageJson = readDriverPackageJson(); const containerfile = readText("../Containerfile"); - const releaseWorkflow = readText("../.github/workflows/release.yml"); + const prWorkflow = readWorkflow("../.github/workflows/pr.yml"); + const releaseWorkflowText = readText("../.github/workflows/release.yml"); + const releaseWorkflow = readWorkflow("../.github/workflows/release.yml"); + const verifyJob = workflowJob(releaseWorkflow, "verify"); + const buildJob = workflowJob(releaseWorkflow, "build"); + const imageJob = workflowJob(releaseWorkflow, "publish-versioned-image"); + const npmJob = workflowJob(releaseWorkflow, "publish-npm"); + const latestJob = workflowJob(releaseWorkflow, "publish-latest-image"); + const verifyRun = workflowStep(verifyJob, "Verify release tag").run ?? ""; + const buildRun = + buildJob.steps?.flatMap(({ run }) => (run === undefined ? [] : [run])).join("\n") ?? ""; + const imageRun = workflowStep(imageJob, "Publish versioned image").run ?? ""; + const npmRun = workflowStep(npmJob, "Publish package").run ?? ""; + const latestRun = + latestJob.steps?.flatMap(({ run }) => (run === undefined ? [] : [run])).join("\n") ?? ""; + const claudeVersion = packageJson.dependencies?.["@anthropic-ai/claude-agent-sdk"]; const openCodeVersion = packageJson.devDependencies?.["opencode-ai"]; - const packIndex = releaseWorkflow.indexOf("- name: Pack package"); - const liveIndex = releaseWorkflow.indexOf("- name: Test packed driver"); - const imageIndex = releaseWorkflow.indexOf("- name: Build image"); + const bunVersion = packageJson.packageManager?.replace("bun@", ""); + const actionUses = [prWorkflow, releaseWorkflow].flatMap((workflow) => + Object.values(workflow.jobs ?? {}).flatMap((job) => + (job.steps ?? []).flatMap((step) => (step.uses === undefined ? [] : [step.uses])), + ), + ); - expect(openCodeVersion).toBe("1.18.4"); + expect(containerfile).toContain(`ARG BUN_VERSION=${bunVersion}`); + expect(containerfile).toContain('RUN test "$(bun --version)" = "$BUN_VERSION"'); + expect(containerfile).toContain(`ARG CLAUDE_AGENT_SDK_VERSION=${claudeVersion}`); expect(containerfile).toContain(`ARG OPENCODE_VERSION=${openCodeVersion}`); - expect(releaseWorkflow).toContain("AGENT_DRIVER_LIVE_ARTIFACT: packed/dist/driver.mjs"); - expect(releaseWorkflow).toContain("--strip-components=1"); - expect(releaseWorkflow).toContain("secrets.OPENROUTER_API_KEY"); - expect(packIndex).toBeGreaterThan(-1); - expect(liveIndex).toBeGreaterThan(packIndex); - expect(imageIndex).toBeGreaterThan(liveIndex); + expect(containerfile).toContain( + "@anthropic-ai/claude-agent-sdk-linux-x64@${CLAUDE_AGENT_SDK_VERSION}", + ); + expect(containerfile).toContain("opencode-linux-x64-baseline@${OPENCODE_VERSION}"); + expect(releaseWorkflow.concurrency).toEqual({ group: "release", queue: "max" }); + expect({ + build: buildJob.needs, + latest: latestJob.needs, + npm: npmJob.needs, + versionedImage: imageJob.needs, + }).toEqual({ + build: "verify", + latest: ["publish-versioned-image", "publish-npm"], + npm: ["build", "publish-versioned-image"], + versionedImage: "build", + }); + expect(latestJob.if).toBe("needs.publish-npm.outputs.promote_latest == 'true'"); + for (const marker of [ + 'version="$(node -p "require(\'./package.json\').version")"', + 'if [[ "${tag}" != "v${version}" ]]', + 'git merge-base --is-ancestor "${GITHUB_SHA}" origin/main', + ]) + expect(verifyRun).toContain(marker); + for (const marker of [ + "npm pack --ignore-scripts", + "declarations=(packed/dist/types/**/*.d.ts)", + "bun test tests/driver-artifact-mcp.test.ts", + "buildah build", + "podman run --pull=never --rm", + ]) + expect(buildRun).toContain(marker); + for (const marker of [ + "remote_digest=", + 'if [[ "$remote_digest" != "$EXPECTED_DIGEST" ]]', + "skopeo copy --preserve-digests", + "gh attestation verify", + ]) + expect(imageRun).toContain(marker); + for (const marker of [ + "remote_integrity=", + 'npm view "$package_name@>$VERSION"', + 'npm publish "$tarball" --ignore-scripts --provenance', + ]) + expect(npmRun).toContain(marker); + for (const marker of [ + 'npm view "$PACKAGE_NAME" dist-tags.latest', + 'npm view "$PACKAGE_NAME@>$VERSION"', + 'skopeo copy --preserve-digests "docker://$IMAGE@$DIGEST" "docker://$IMAGE:latest"', + ]) + expect(latestRun).toContain(marker); + expect(releaseWorkflowText).not.toContain("OPENROUTER_API_KEY"); + expect(releaseWorkflowText).not.toContain("test:live:artifact"); + expect(actionUses.length).toBeGreaterThan(0); + for (const uses of actionUses) { + expect(uses).toMatch(/^[\w.-]+\/[\w./-]+@[0-9a-f]{40}$/); + } }); test("declares writable Environment package managers", () => { diff --git a/tests/driver-artifact-live.test.ts b/tests/driver-artifact-live.test.ts index a30b404..b4e69ff 100644 --- a/tests/driver-artifact-live.test.ts +++ b/tests/driver-artifact-live.test.ts @@ -282,7 +282,7 @@ function readLiveConfig(): LiveConfig { assertCommandVersion( openAiCommand, ["--version"], - resolve(process.cwd(), "node_modules", "@openai", "codex-sdk", "package.json"), + resolve(process.cwd(), "node_modules", "@openai", "codex", "package.json"), "OpenAI app-server", ); } @@ -343,16 +343,14 @@ const runtimeCases: LiveRuntimeCase[] = [ suite: "anthropic", transport: "claude-agent-sdk", } satisfies LiveRuntimeCase, - ...config.openCodeModels.map( - (model): LiveRuntimeCase => ({ - model, - nativeResumeKind: "acp_session_id", - provider: "openrouter", - runtime: "acp-fallback", - suite: "opencode", - transport: "acp-fallback", - }), - ), + ...config.openCodeModels.map((model): LiveRuntimeCase => ({ + model, + nativeResumeKind: "acp_session_id", + provider: "openrouter", + runtime: "acp-fallback", + suite: "opencode", + transport: "acp-fallback", + })), ].filter((runtimeCase) => config.suite === "all" || config.suite === runtimeCase.suite); const representativeOpenCodeModel = config.openCodeModels.find((model) => model.includes("/deepseek/")) ?? config.openCodeModels[0]!; @@ -383,27 +381,64 @@ function payloadRecord(event: DriverArtifactTestEvent): Record : {}; } +function messageText(events: readonly DriverArtifactTestEvent[], messageId: string): string { + let text = ""; + + for (const event of events) { + const payload = payloadRecord(event); + if (payload["messageId"] !== messageId) { + continue; + } + + if (event.kind === "message.delta" && typeof payload["contentDelta"] === "string") { + text += payload["contentDelta"]; + continue; + } + + if (event.kind !== "message.added") { + continue; + } + + const content = payload["content"]; + text = + typeof content === "string" + ? content + : Array.isArray(content) + ? content + .flatMap((block) => { + const value = + typeof block === "object" && block !== null && !Array.isArray(block) + ? (block as Record)["text"] + : null; + return typeof value === "string" ? [value] : []; + }) + .join("") + : text; + } + + return text; +} + function eventText(events: readonly DriverArtifactTestEvent[]): string { - const finalText = events + const finalMessageId = events .filter((event) => event.kind === "run.completed") - .map((event) => payloadRecord(event)["finalMessageText"]) + .map((event) => payloadRecord(event)["finalMessageId"]) .findLast((value): value is string => typeof value === "string" && value.length > 0); - if (finalText !== undefined) { - return finalText; + if (finalMessageId !== undefined) { + return messageText(events, finalMessageId); } - return events - .flatMap((event) => { - const payload = payloadRecord(event); - - if (event.kind === "message.delta" && typeof payload["contentDelta"] === "string") { - return [payload["contentDelta"]]; + const messageIds = new Set( + events.flatMap((event) => { + if (event.kind !== "message.added" && event.kind !== "message.delta") { + return []; } - - return []; - }) - .join(""); + const messageId = payloadRecord(event)["messageId"]; + return typeof messageId === "string" ? [messageId] : []; + }), + ); + return [...messageIds].map((messageId) => messageText(events, messageId)).join(""); } function eventOutputText(events: readonly DriverArtifactTestEvent[]): string { @@ -693,9 +728,8 @@ function expectSingleRunLifecycle( if (expectations.requireFinalMessage) { const terminal = terminalEvents[0]!; const finalMessageId = payloadString(terminal, "finalMessageId"); - const finalMessageText = payloadString(terminal, "finalMessageText"); expect(finalMessageId).not.toBeNull(); - expect(finalMessageText).not.toBeNull(); + expect(payloadRecord(terminal)).not.toHaveProperty("finalMessageText"); expect( runEvents.some( (event) => @@ -703,47 +737,8 @@ function expectSingleRunLifecycle( payloadString(event, "messageId") === finalMessageId, ), ).toBe(true); - const completedAgentMessageIds = runEvents - .filter( - (event) => event.kind === "message.completed" && payloadRecord(event)["role"] === "agent", - ) - .map((event) => payloadString(event, "messageId")) - .filter((messageId): messageId is string => messageId !== null); - expect(completedAgentMessageIds.length).toBeGreaterThan(0); - expect(finalMessageId).toBe(completedAgentMessageIds.at(-1)); - - let reconstructedFinalMessageText = ""; - for (const event of runEvents.filter( - (candidate) => - payloadString(candidate, "messageId") === finalMessageId && - (candidate.kind === "message.added" || candidate.kind === "message.delta"), - )) { - const payload = payloadRecord(event); - if (event.kind === "message.delta") { - const contentDelta = payloadString(event, "contentDelta"); - expect(contentDelta).not.toBeNull(); - reconstructedFinalMessageText += contentDelta ?? ""; - continue; - } - - const content = payload["content"]; - reconstructedFinalMessageText = - typeof content === "string" - ? content - : Array.isArray(content) - ? content - .flatMap((block) => { - const text = - typeof block === "object" && block !== null && !Array.isArray(block) - ? (block as Record)["text"] - : null; - return typeof text === "string" ? [text] : []; - }) - .join("") - : reconstructedFinalMessageText; - } + const reconstructedFinalMessageText = messageText(runEvents, finalMessageId!); expect(reconstructedFinalMessageText.length).toBeGreaterThan(0); - expect(finalMessageText).toBe(reconstructedFinalMessageText); } } diff --git a/tests/driver-artifact-mcp.test.ts b/tests/driver-artifact-mcp.test.ts index 39c401e..25f32fb 100644 --- a/tests/driver-artifact-mcp.test.ts +++ b/tests/driver-artifact-mcp.test.ts @@ -206,6 +206,13 @@ artifactTest( }; methods.push(message.method); + if (message.method === "server/discover") { + return Response.json({ + error: { code: -32601, message: "Method not found" }, + id: message.id, + jsonrpc: "2.0", + }); + } if (message.method !== "initialize" && !hasValidSessionHeaders()) { invalidSessionHeaders += 1; return new Response("Invalid MCP session headers.", { status: 400 }); @@ -444,7 +451,14 @@ artifactTest( controller.enqueue(mcpCommand("mcp-structured", "structured")); expect(await controller.waitForCommandTerminal("mcp-structured", 10_000)).toMatchObject({ result: { - outputText: '{\n "count": 1,\n "status": "ok"\n}', + outputText: JSON.stringify( + { + content: [], + structuredContent: { count: 1, status: "ok" }, + }, + null, + 2, + ), }, status: "completed", }); diff --git a/tests/driver-artifact-test-controller.ts b/tests/driver-artifact-test-controller.ts index 982a224..8b6a931 100644 --- a/tests/driver-artifact-test-controller.ts +++ b/tests/driver-artifact-test-controller.ts @@ -17,7 +17,7 @@ import { parseDriverReadyInput, type DriverLogEntry, } from "../src/protocol/orpc"; -import type { DriverCapability } from "../src/runtime-command"; +import type { DriverCapability, McpExecuteCommandResult } from "../src/runtime-command"; import { PROCESS_TREE_OWNER_ENV } from "../src/runtimes/child-process"; import { AGENT_DRIVER_PROVIDER_REGISTRY, @@ -298,6 +298,12 @@ export class DriverArtifactTestController { readonly #eventIngressGates = new Set(); readonly #eventIngressObservers = new Set<(event: DriverArtifactTestEvent) => void>(); readonly #events: DriverArtifactTestEvent[] = []; + readonly #externalToolEffects = new Map< + string, + | { readonly status: "executing" } + | { readonly result: McpExecuteCommandResult; readonly status: "completed" } + | { readonly status: "unknown" } + >(); readonly #expectedCapabilities: readonly DriverCapability[] | undefined; readonly #forbiddenSecrets: readonly string[]; readonly #heartbeatIntervalMs: number; @@ -996,6 +1002,78 @@ export class DriverArtifactTestController { }); return { ok: true }; } + case "/driver/claimExternalToolEffect": { + const commandId = readString(input, "commandId", "external tool effect claim"); + this.#assertDriverInstanceId( + readString(input, "driverInstanceId", "external tool effect claim"), + ); + const effectId = `artifact-test-effect-${commandId}`; + const effect = this.#externalToolEffects.get(commandId); + + if (effect?.status === "completed") { + return { effectId, kind: "completed", result: structuredClone(effect.result) }; + } + if (effect?.status === "unknown") { + return { effectId, kind: "unknown" }; + } + + this.#externalToolEffects.set(commandId, { status: "executing" }); + return { attempt: 1, effectId, idempotencyKey: effectId, kind: "execute" }; + } + case "/driver/completeExternalToolEffect": { + const commandId = readString(input, "commandId", "external tool effect completion"); + const driverInstanceId = readString( + input, + "driverInstanceId", + "external tool effect completion", + ); + this.#assertDriverInstanceId(driverInstanceId); + const parsed = parseDriverCommandUpdateInput({ + commandId, + driverInstanceId, + result: input["result"], + status: "completed", + }); + + if ( + parsed.result === undefined || + parsed.result === null || + !("outputText" in parsed.result) + ) { + throw new Error("External tool effect completion requires an MCP result."); + } + + const current = this.#externalToolEffects.get(commandId); + if (current?.status === "completed") { + if (JSON.stringify(current.result) !== JSON.stringify(parsed.result)) { + throw new Error(`External tool effect ${commandId} completed with a different result.`); + } + return { ok: true }; + } + if (current?.status !== "executing") { + throw new Error(`External tool effect ${commandId} is not executing.`); + } + + this.#externalToolEffects.set(commandId, { + result: structuredClone(parsed.result), + status: "completed", + }); + return { ok: true }; + } + case "/driver/markExternalToolEffectUnknown": { + const commandId = readString(input, "commandId", "external tool effect uncertainty"); + this.#assertDriverInstanceId( + readString(input, "driverInstanceId", "external tool effect uncertainty"), + ); + const current = this.#externalToolEffects.get(commandId); + + if (current?.status === "completed") { + throw new Error(`Completed external tool effect ${commandId} cannot become unknown.`); + } + + this.#externalToolEffects.set(commandId, { status: "unknown" }); + return { ok: true }; + } case "/driver/completeRun": { const completion = parseDriverCompletionInput(input); this.#assertDriverInstanceId(completion.driverInstanceId); diff --git a/tests/driver-event-publisher-admission.test.ts b/tests/driver-event-publisher-admission.test.ts index 4fc7c95..e708704 100644 --- a/tests/driver-event-publisher-admission.test.ts +++ b/tests/driver-event-publisher-admission.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from "bun:test"; -import { pushLosslessEvents, withSourceEventIds } from "../src/core/driver-runtime-io"; +import { + DriverEventRejectedError, + pushLosslessEvents, + withSourceEventIds, +} from "../src/core/driver-runtime-io"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; @@ -61,6 +65,19 @@ function kinds(batches: readonly (readonly DriverEventInput[])[]): string[][] { return batches.map((batch) => batch.map((event) => event.kind)); } +function acceptEvents( + events: readonly DriverEventInput[], + sequence: (index: number) => number = (index) => index + 1, +): DriverEventBatchOutput { + return { + accepted: events.map((event, index) => ({ + eventId: event.sourceEventId, + seq: sequence(index), + type: event.kind, + })), + }; +} + function createContext(input: { currentRunId?: () => RunId | null; pushEvents: (events: DriverEventInput[], signal?: AbortSignal) => Promise; @@ -68,7 +85,7 @@ function createContext(input: { return createAgentDriverContext({ eventSink: { commandUpdate: async () => {}, - ...(input.currentRunId === undefined ? {} : { currentRunId: input.currentRunId }), + currentRunId: input.currentRunId ?? (() => DRIVER_TEST_IDS.runId), pushEvents: async ({ events, signal }) => input.pushEvents(events, signal), }, logger: createTestLogger(), @@ -91,13 +108,7 @@ describe("DriverEventPublisher", () => { throw new Error("response lost"); } - return { - accepted: sent.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; + return acceptEvents(sent); }, }; @@ -108,6 +119,46 @@ describe("DriverEventPublisher", () => { expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); }); + test("retries one transport failure after a valid receipt prefix", async () => { + const attempts: DriverEventInput[][] = []; + let sequence = 0; + const port = { + pushEvents: async ({ events }: { events: DriverEventInput[] }) => { + attempts.push(events); + + if (attempts.length === 1) { + return { + accepted: [ + { + eventId: events[0]!.sourceEventId, + seq: ++sequence, + type: events[0]!.kind, + }, + ], + }; + } + + if (attempts.length === 2) { + throw new Error("temporary transport failure"); + } + + return acceptEvents(events, () => ++sequence); + }, + }; + + await expect( + pushLosslessEvents(port, [createEvent("message.started"), createEvent("message.completed")]), + ).resolves.toHaveLength(2); + + expect(kinds(attempts)).toEqual([ + ["message.started", "message.completed"], + ["message.completed"], + ["message.completed"], + ]); + expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[1]?.sourceEventId); + expect(attempts[2]?.[0]?.sourceEventId).toBe(attempts[0]?.[1]?.sourceEventId); + }); + test.each([ ["without", undefined], ["with", "explicit-source-event-id"], @@ -145,13 +196,7 @@ describe("DriverEventPublisher", () => { }; } - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 2, - type: event.kind, - })), - }; + return acceptEvents(events, (index) => index + 2); }, }; @@ -228,150 +273,6 @@ describe("DriverEventPublisher", () => { await context.logger.destroy(); }); - test.each(["run.cancelled", "run.completed", "run.failed"] as const)( - "admits one %s event after the lossless queue is full", - async (kind) => { - const attempts: DriverEventInput[][] = []; - const context = createContext({ - pushEvents: async (events) => { - attempts.push(events); - - if (attempts.length === 1) { - throw new Error("socket unavailable"); - } - - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const retained = Array.from({ length: 1_024 }, () => createEvent("message.started")); - - await expect(publisher.push(context, "fill", retained)).rejects.toThrow("socket unavailable"); - await expect( - publisher.push(context, "terminal", [createRunTerminal(kind)]), - ).resolves.toBeUndefined(); - - expect(attempts).toHaveLength(2); - expect(attempts[1]).toHaveLength(1_025); - expect(attempts[1]?.at(-1)?.kind).toBe(kind); - await context.logger.destroy(); - }, - ); - - test("reserves exactly one run terminal slot", async () => { - let sends = 0; - const context = createContext({ - pushEvents: async () => { - sends += 1; - return { accepted: [] }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - - await expect( - publisher.push(context, "terminal", [ - createRunTerminal("run.completed"), - createRunTerminal("run.failed"), - ]), - ).rejects.toThrow("run terminal slot"); - expect(sends).toBe(0); - await context.logger.destroy(); - }); - - test("bounds the independent run terminal slot by bytes", async () => { - let sends = 0; - const context = createContext({ - pushEvents: async () => { - sends += 1; - return { accepted: [] }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const terminal = { - ...createRunTerminal("run.completed"), - payload: { detail: "x".repeat(1_024 * 1_024) }, - }; - - await expect(publisher.push(context, "terminal", [terminal])).rejects.toThrow( - "run terminal batch exceeds 1048576 UTF-8 bytes", - ); - expect(sends).toBe(0); - await context.logger.destroy(); - }); - - test("admits a bounded closing batch through the full regular lossless lane", async () => { - const attempts: DriverEventInput[][] = []; - const context = createContext({ - pushEvents: async (events) => { - attempts.push(events); - - if (attempts.length === 1) { - throw new Error("socket unavailable"); - } - - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const retained = Array.from({ length: 1_024 }, () => createEvent("message.started")); - const closing = [ - createEvent("message.completed"), - { - kind: "thought.completed", - payload: { channel: "summary", thoughtId: "thought-1" }, - } satisfies DriverEventInput, - createRunTerminal("run.completed"), - ]; - - await expect(publisher.push(context, "fill", retained)).rejects.toThrow("socket unavailable"); - await expect(publisher.push(context, "terminal", closing)).resolves.toBeUndefined(); - - expect(attempts[1]).toHaveLength(1_027); - expect(attempts[1]?.slice(-3).map((event) => event.kind)).toEqual([ - "message.completed", - "thought.completed", - "run.completed", - ]); - await context.logger.destroy(); - }); - - test("drops an oversized best-effort prefix without blocking its run terminal", async () => { - const attempts: DriverEventInput[][] = []; - const context = createContext({ - pushEvents: async (events) => { - attempts.push(events); - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - - await publisher.push(context, "terminal", [ - ...Array.from({ length: 2_048 }, (_, index) => createDelta(String(index))), - createRunTerminal("run.completed"), - ]); - - expect(kinds(attempts)).toEqual([["run.completed"]]); - await context.logger.destroy(); - }); - test("does not let a timed-out best-effort lane poison later lossless delivery", async () => { const nativeTimeout = AbortSignal.timeout; const signals: (AbortSignal | undefined)[] = []; @@ -391,13 +292,7 @@ describe("DriverEventPublisher", () => { signal?.throwIfAborted(); } - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; + return acceptEvents(events); }, }); const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); @@ -479,34 +374,6 @@ describe("DriverEventPublisher", () => { await logger.destroy(); }); - test("coalesces concurrent singleton pushes without starving the final terminal", async () => { - const attempts: DriverEventInput[][] = []; - const context = createContext({ - pushEvents: async (events) => { - attempts.push(events); - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const pushes = Array.from({ length: 1_024 }, () => - publisher.push(context, "singleton", [createEvent("message.started")]), - ); - pushes.push(publisher.push(context, "terminal", [createRunTerminal("run.completed")])); - - await Promise.all(pushes); - - expect(attempts).toHaveLength(1); - expect(attempts[0]).toHaveLength(1_025); - expect(attempts[0]?.at(-1)?.kind).toBe("run.completed"); - await context.logger.destroy(); - }); - test("does not block best-effort producers on transport acknowledgements", async () => { const firstSendEntered = Promise.withResolvers(); const releaseFirstSend = Promise.withResolvers(); @@ -515,13 +382,7 @@ describe("DriverEventPublisher", () => { firstSendEntered.resolve(); await releaseFirstSend.promise; - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; + return acceptEvents(events); }, }); const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); @@ -536,22 +397,1044 @@ describe("DriverEventPublisher", () => { await context.logger.destroy(); }); - test("bounds the number of closing events in a run terminal batch", async () => { - let sends = 0; + test("delivers terminal closures one at a time before the run terminal", async () => { + const attempts: DriverEventInput[][] = []; + let seq = 0; const context = createContext({ - pushEvents: async () => { - sends += 1; - return { accepted: [] }; + pushEvents: async (events) => { + attempts.push(events); + return acceptEvents(events, () => (seq += 1)); }, }); const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closures = Array.from({ length: 66 }, (_, index) => ({ + kind: "message.completed", + payload: { messageId: `message-${index}`, stopReason: "end_turn" }, + })) satisfies DriverEventInput[]; - await expect( - publisher.push(context, "terminal", [ - ...Array.from({ length: 64 }, () => createEvent("message.completed")), + await publisher.pushTerminal(context, "terminal", closures, createRunTerminal("run.completed")); + + expect(attempts).toHaveLength(67); + expect(attempts.every((events) => events.length === 1)).toBe(true); + expect(attempts.slice(0, -1).every(([event]) => event?.kind === "message.completed")).toBe( + true, + ); + expect(attempts.at(-1)?.[0]?.kind).toBe("run.completed"); + await context.logger.destroy(); + }); + + test("bounds the whole terminal settlement by one delivery deadline", async () => { + const acceptedKinds: string[] = []; + const attempts: DriverEventInput[][] = []; + const nativeTimeout = AbortSignal.timeout; + AbortSignal.timeout = () => nativeTimeout(100); + const context = createContext({ + pushEvents: async (events, signal) => { + attempts.push(events); + await new Promise((resolve, reject) => { + const timeout = setTimeout(resolve, 40); + signal?.addEventListener( + "abort", + () => { + clearTimeout(timeout); + reject(signal.reason); + }, + { once: true }, + ); + }); + acceptedKinds.push(...events.map(({ kind }) => kind)); + return acceptEvents(events, (index) => attempts.length + index); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + let terminal: Promise; + + try { + terminal = publisher.pushTerminal( + context, + "terminal", + [ + { ...createEvent("message.completed"), payload: { messageId: "one" } }, + { ...createEvent("message.completed"), payload: { messageId: "two" } }, + ], createRunTerminal("run.completed"), - ]), - ).rejects.toThrow("run terminal batch exceeds 64 events"); + ); + } finally { + AbortSignal.timeout = nativeTimeout; + } + + try { + await expect(terminal).rejects.toThrow(); + expect(kinds(attempts)).toEqual([ + ["message.completed"], + ["message.completed"], + ["run.completed"], + ]); + expect(acceptedKinds).toEqual(["message.completed", "message.completed"]); + + AbortSignal.timeout = () => nativeTimeout(100); + try { + terminal = publisher.pushTerminal( + context, + "terminal.retry", + [ + { ...createEvent("message.completed"), payload: { messageId: "one" } }, + { ...createEvent("message.completed"), payload: { messageId: "two" } }, + ], + createRunTerminal("run.completed"), + ); + } finally { + AbortSignal.timeout = nativeTimeout; + } + await expect(terminal).resolves.toBeUndefined(); + expect(acceptedKinds).toEqual(["message.completed", "message.completed", "run.completed"]); + } finally { + await context.logger.destroy(); + } + }); + + test("bounds waiting for an earlier drain by the terminal settlement deadline", async () => { + const attempts: DriverEventInput[][] = []; + const drainEntered = Promise.withResolvers(); + const releaseDrain = Promise.withResolvers(); + const nativeTimeout = AbortSignal.timeout; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + if (attempts.length === 1) { + drainEntered.resolve(); + await releaseDrain.promise; + } + return acceptEvents(events, (index) => attempts.length + index); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const ordinary = publisher.push(context, "ordinary", [createEvent("message.completed")]); + await drainEntered.promise; + AbortSignal.timeout = () => nativeTimeout(50); + let terminal: Promise; + + try { + terminal = publisher.pushTerminal( + context, + "terminal", + [], + createRunTerminal("run.completed"), + ); + } finally { + AbortSignal.timeout = nativeTimeout; + } + + try { + await expect(terminal).rejects.toThrow(); + expect(kinds(attempts)).toEqual([["message.completed"]]); + } finally { + releaseDrain.resolve(); + await ordinary; + await context.logger.destroy(); + } + }); + + test("retries the exact pending closure before delivering the run terminal", async () => { + const attempts: DriverEventInput[][] = []; + let seq = 0; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + throw new Error("response lost"); + } + + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + + await publisher.pushTerminal( + context, + "terminal", + [createEvent("message.completed")], + createRunTerminal("run.completed"), + ); + + expect(kinds(attempts)).toEqual([ + ["message.completed"], + ["message.completed"], + ["run.completed"], + ]); + expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); + await context.logger.destroy(); + }); + + test("reuses a retained closure identity when the terminal call is retried", async () => { + const attempts: DriverEventInput[][] = []; + const accepted: DriverEventInput[] = []; + let seq = 0; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length <= 2) { + throw new Error(`socket unavailable ${attempts.length}`); + } + + accepted.push(...events); + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closures = [createEvent("message.completed")]; + const terminal = createRunTerminal("run.completed"); + + await expect(publisher.pushTerminal(context, "terminal", closures, terminal)).rejects.toThrow( + "socket unavailable 2", + ); + await expect( + publisher.pushTerminal(context, "terminal.retry", closures, terminal), + ).resolves.toBeUndefined(); + + expect(kinds(attempts)).toEqual([ + ["message.completed"], + ["message.completed"], + ["message.completed"], + ["run.completed"], + ]); + expect(new Set(attempts.slice(0, 3).map(([event]) => event?.sourceEventId)).size).toBe(1); + expect(kinds([accepted])).toEqual([["message.completed", "run.completed"]]); + await context.logger.destroy(); + }); + + test("joins one in-flight terminal operation and rejects a different occurrence", async () => { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + let seq = 0; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + entered.resolve(); + await release.promise; + } + + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closure = createEvent("message.completed"); + const terminal = createRunTerminal("run.completed"); + const first = publisher.pushTerminal(context, "terminal", [closure], terminal); + const joined = publisher.pushTerminal(context, "terminal.join", [closure], terminal); + + await entered.promise; + expect(() => + publisher.pushTerminal( + context, + "terminal.other", + [{ ...closure, sourceEventId: "explicit-other-closure" }], + { ...terminal, sourceEventId: "explicit-other-terminal" }, + ), + ).toThrow("settlement slot is full"); + release.resolve(); + await Promise.all([first, joined]); + + expect(kinds(attempts)).toEqual([["message.completed"], ["run.completed"]]); + await context.logger.destroy(); + }); + + test("continues a partially delivered terminal operation without duplicating closures", async () => { + const attempts: DriverEventInput[][] = []; + const accepted: DriverEventInput[] = []; + let seq = 0; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 2 || attempts.length === 3) { + throw new Error(`closure unavailable ${attempts.length}`); + } + + accepted.push(...events); + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closures = [ + { ...createEvent("message.completed"), payload: { messageId: "one" } }, + { ...createEvent("message.completed"), payload: { messageId: "two" } }, + ] satisfies DriverEventInput[]; + const terminal = createRunTerminal("run.completed"); + + await expect(publisher.pushTerminal(context, "terminal", closures, terminal)).rejects.toThrow( + "closure unavailable 3", + ); + await publisher.pushTerminal(context, "terminal.retry", closures, terminal); + + expect(accepted.map((event) => event.kind)).toEqual([ + "message.completed", + "message.completed", + "run.completed", + ]); + expect( + accepted.filter( + (event) => + event.kind === "message.completed" && + (event.payload as { messageId?: string }).messageId === "one", + ), + ).toHaveLength(1); + expect(new Set(attempts.slice(1, 4).map(([event]) => event?.sourceEventId)).size).toBe(1); + await context.logger.destroy(); + }); + + test("adopts the identity of a matching generic pending closure", async () => { + const attempts: DriverEventInput[][] = []; + let seq = 0; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + throw new Error("generic closure unavailable"); + } + + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closure = createEvent("message.completed"); + + await expect(publisher.push(context, "closure", [closure])).rejects.toThrow( + "generic closure unavailable", + ); + await publisher.pushTerminal( + context, + "terminal", + [closure], + createRunTerminal("run.completed"), + ); + + expect(kinds(attempts)).toEqual([ + ["message.completed"], + ["message.completed"], + ["run.completed"], + ]); + expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); + await context.logger.destroy(); + }); + + test("does not duplicate a matching closure already in flight", async () => { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + let seq = 0; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + entered.resolve(); + await release.promise; + } + + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closure = createEvent("message.completed"); + const ordinary = publisher.push(context, "closure", [closure]); + + await entered.promise; + const terminal = publisher.pushTerminal( + context, + "terminal", + [closure], + createRunTerminal("run.completed"), + ); + void ordinary.catch(() => {}); + void terminal.catch(() => {}); + release.resolve(); + await Promise.all([ordinary, terminal]); + + expect(kinds(attempts)).toEqual([["message.completed"], ["run.completed"]]); + await context.logger.destroy(); + }); + + test("does not resend an accepted in-flight closure while its suffix is blocked", async () => { + const suffixEntered = Promise.withResolvers(); + const releaseSuffix = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const accepted: DriverEventInput[] = []; + let seq = 0; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + accepted.push(events[0]!); + return { + accepted: [ + { + eventId: events[0]!.sourceEventId, + seq: (seq += 1), + type: events[0]!.kind, + }, + ], + }; + } + + if (attempts.length === 2) { + suffixEntered.resolve(); + await releaseSuffix.promise; + throw new Error("suffix transport unavailable"); + } + + if (attempts.length === 3) { + throw new Error("suffix transport still unavailable"); + } + + accepted.push(...events); + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closure = createEvent("message.completed"); + const ordinary = publisher.push(context, "ordinary", [closure, createEvent("message.started")]); + + await suffixEntered.promise; + const terminal = publisher.pushTerminal( + context, + "terminal", + [closure], + createRunTerminal("run.completed"), + ); + void ordinary.catch(() => {}); + void terminal.catch(() => {}); + releaseSuffix.resolve(); + + await expect(ordinary).rejects.toThrow("suffix transport still unavailable"); + await expect(terminal).resolves.toBeUndefined(); + expect(kinds(attempts)).toEqual([ + ["message.completed", "message.started"], + ["message.started"], + ["message.started"], + ["message.started", "run.completed"], + ]); + expect( + accepted.filter((event) => event.sourceEventId === attempts[0]![0]!.sourceEventId), + ).toHaveLength(1); + await context.logger.destroy(); + }); + + test("does not revive a matching in-flight closure rejected by the sink", async () => { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + entered.resolve(); + await release.promise; + throw new DriverEventRejectedError( + events[0]!.sourceEventId!, + new Error("closure rejected"), + ); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closure = createEvent("message.completed"); + const ordinary = publisher.push(context, "closure", [closure]); + + await entered.promise; + const terminal = publisher.pushTerminal( + context, + "terminal", + [closure], + createRunTerminal("run.completed"), + ); + void ordinary.catch(() => {}); + void terminal.catch(() => {}); + release.resolve(); + + await expect(ordinary).rejects.toThrow("closure rejected"); + await expect(terminal).rejects.toThrow("closure rejected"); + expect(kinds(attempts)).toEqual([["message.completed"]]); + await context.logger.destroy(); + }); + + test("does not revive a rejected closure while its in-flight suffix is blocked", async () => { + const suffixEntered = Promise.withResolvers(); + const releaseSuffix = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + let seq = 0; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + throw new DriverEventRejectedError( + events[0]!.sourceEventId!, + new Error("closure rejected"), + ); + } + + suffixEntered.resolve(); + await releaseSuffix.promise; + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closure = createEvent("message.completed"); + const ordinary = publisher.push(context, "ordinary", [closure, createEvent("message.started")]); + + await suffixEntered.promise; + const terminal = publisher.pushTerminal( + context, + "terminal", + [closure], + createRunTerminal("run.completed"), + ); + void ordinary.catch(() => {}); + void terminal.catch(() => {}); + releaseSuffix.resolve(); + + await expect(ordinary).rejects.toThrow("closure rejected"); + await expect(terminal).rejects.toThrow("closure rejected"); + expect(kinds(attempts)).toEqual([ + ["message.completed", "message.started"], + ["message.started"], + ]); + await context.logger.destroy(); + }); + + test("does not treat a rejected pending terminal retry as accepted", async () => { + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + throw new Error("response lost"); + } + + throw new DriverEventRejectedError( + events[0]!.sourceEventId!, + new Error("terminal rejected"), + ); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + + await expect( + publisher.pushTerminal(context, "terminal", [], createRunTerminal("run.completed")), + ).rejects.toThrow("terminal rejected"); + expect(kinds(attempts)).toEqual([["run.completed"], ["run.completed"]]); + expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); + await context.logger.destroy(); + }); + + test.each([ + ["target", true], + ["unrelated", false], + ] as const)( + "attributes a coalesced pending rejection to the %s event", + async (_name, rejectTarget) => { + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + throw new Error("coalesced transport failure"); + } + + if (attempts.length === 2) { + const rejected = rejectTarget + ? events.find((event) => event.kind === "message.completed") + : events.find((event) => event.kind === "message.started"); + throw new DriverEventRejectedError( + rejected!.sourceEventId!, + new Error(rejectTarget ? "target rejected" : "unrelated rejected"), + ); + } + + if (rejectTarget && attempts.length === 3) { + throw new Error("unrelated remains pending"); + } + + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const unrelated = publisher.push(context, "unrelated", [createEvent("message.started")]); + const terminal = publisher.pushTerminal( + context, + "terminal", + [createEvent("message.completed")], + createRunTerminal("run.completed"), + ); + void unrelated.catch(() => {}); + void terminal.catch(() => {}); + + await expect(unrelated).rejects.toThrow("coalesced transport failure"); + + if (rejectTarget) { + await expect(terminal).rejects.toThrow("target rejected"); + expect(attempts.flat().some((event) => event.kind === "run.completed")).toBe(false); + } else { + await expect(terminal).resolves.toBeUndefined(); + expect(attempts.at(-1)?.[0]?.kind).toBe("run.completed"); + } + + await context.logger.destroy(); + }, + ); + + test("does not mistake an old full pending queue for an admitted closure", async () => { + const retryEntered = Promise.withResolvers(); + const releaseRetry = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + throw new Error("socket unavailable"); + } + + if (attempts.length === 2) { + retryEntered.resolve(); + await releaseRetry.promise; + } + + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const retained = Array.from({ length: 1_024 }, (_, index) => ({ + kind: "message.started", + payload: { messageId: `pending-${index}`, role: "agent" }, + })) satisfies DriverEventInput[]; + + try { + await expect(publisher.push(context, "fill", retained)).rejects.toThrow("socket unavailable"); + await expect( + publisher.pushTerminal( + context, + "terminal", + [createEvent("message.completed")], + createRunTerminal("run.completed"), + ), + ).rejects.toThrow("Driver event queue exceeds 1024 events"); + await retryEntered.promise; + } finally { + releaseRetry.resolve(); + await context.logger.destroy(); + } + + expect(attempts.flat().some((event) => event.kind === "run.completed")).toBe(false); + }); + + test("does not retry a rejected closure through unrelated pending events", async () => { + const firstSendEntered = Promise.withResolvers(); + const releaseFirstSend = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + firstSendEntered.resolve(); + await releaseFirstSend.promise; + throw new Error("initial transport failure"); + } + + if (attempts.length === 3) { + throw new Error("old pending still unavailable"); + } + + const envelopes = events.flatMap((event) => + toDriverEventEnvelopes(driverBootPayload, event, DRIVER_TEST_IDS.runId), + ); + return { + accepted: envelopes.map((envelope, index) => ({ + eventId: envelope.eventId, + seq: index + 1, + type: envelope.event.kind, + })), + }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const pending = publisher.push(context, "pending", [createEvent("message.started")]); + void pending.catch(() => {}); + + await firstSendEntered.promise; + const terminal = publisher.pushTerminal( + context, + "terminal", + [{ kind: "invalid.kind", payload: {} } as unknown as DriverEventInput], + createRunTerminal("run.completed"), + ); + void terminal.catch(() => {}); + releaseFirstSend.resolve(); + + await expect(pending).rejects.toThrow("initial transport failure"); + await expect(terminal).rejects.toThrow("unsupported"); + expect(attempts.flat().some((event) => event.kind === "run.completed")).toBe(false); + await context.logger.destroy(); + }); + + test("rejects an oversized closure without delivering the run terminal", async () => { + let sends = 0; + const context = createContext({ + pushEvents: async () => { + sends += 1; + return { accepted: [] }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const oversized = { + kind: "message.completed", + payload: { + messageId: "message-1", + metadata: { detail: "x".repeat(32 * 1_024 * 1_024) }, + stopReason: "end_turn", + }, + } satisfies DriverEventInput; + + expect(() => + publisher.pushTerminal( + context, + "terminal", + [createEvent("message.completed"), oversized], + createRunTerminal("run.completed"), + ), + ).toThrow("Driver event queue exceeds 33554432 UTF-8 bytes"); + expect(sends).toBe(0); + await context.logger.destroy(); + }); + + test("rejects an oversized run terminal before delivering closures", async () => { + let sends = 0; + const context = createContext({ + pushEvents: async () => { + sends += 1; + return { accepted: [] }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const terminal = { + ...createRunTerminal("run.completed"), + payload: { structuredOutput: "x".repeat(1_024 * 1_024) }, + } satisfies DriverEventInput; + + expect(() => + publisher.pushTerminal(context, "terminal", [createEvent("message.completed")], terminal), + ).toThrow("run terminal batch exceeds 1048576 UTF-8 bytes"); + expect(sends).toBe(0); + await context.logger.destroy(); + }); + + test("rejects a non-run terminal before delivering closures", async () => { + let sends = 0; + const context = createContext({ + pushEvents: async () => { + sends += 1; + return { accepted: [] }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + + expect(() => + publisher.pushTerminal( + context, + "terminal", + [createEvent("message.completed")], + createEvent("message.completed"), + ), + ).toThrow("requires a run terminal event"); + expect(sends).toBe(0); + await context.logger.destroy(); + }); + + test.each([ + [ + "closures", + [ + { ...createEvent("message.started"), sourceEventId: "duplicate" }, + { ...createEvent("message.completed"), sourceEventId: "duplicate" }, + ], + createRunTerminal("run.completed"), + ], + [ + "closure and terminal", + [{ ...createEvent("message.completed"), sourceEventId: "duplicate" }], + { ...createRunTerminal("run.completed"), sourceEventId: "duplicate" }, + ], + ] as const)("rejects duplicate source IDs across %s", async (_name, closures, terminal) => { + let sends = 0; + const context = createContext({ + pushEvents: async () => { + sends += 1; + return { accepted: [] }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + + expect(() => publisher.pushTerminal(context, "terminal", closures, terminal)).toThrow( + "requires unique source event IDs", + ); + expect(sends).toBe(0); + await context.logger.destroy(); + }); + + test("rejects duplicate source IDs in an ordinary push before delivery", async () => { + let sends = 0; + const context = createContext({ + pushEvents: async () => { + sends += 1; + return { accepted: [] }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + + await expect( + publisher.push(context, "duplicate", [ + { ...createEvent("message.started"), sourceEventId: "duplicate" }, + { ...createEvent("message.completed"), sourceEventId: "duplicate" }, + ]), + ).rejects.toThrow("requires unique source event IDs"); + expect(sends).toBe(0); + await context.logger.destroy(); + }); + + test("does not confuse a pending event with a different terminal closure sharing its ID", async () => { + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + throw new Error("transport unavailable"); + } + + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const pending = publisher.push(context, "pending", [ + { ...createEvent("message.started"), sourceEventId: "collision" }, + ]); + + await expect(pending).rejects.toThrow("transport unavailable"); + expect(() => + publisher.pushTerminal( + context, + "terminal", + [{ ...createEvent("message.completed"), sourceEventId: "collision" }], + createRunTerminal("run.completed"), + ), + ).toThrow("source event ID conflicts"); + + expect(attempts.flat().some((event) => event.kind === "message.completed")).toBe(false); + expect(attempts.flat().some((event) => event.kind === "run.completed")).toBe(false); + await context.logger.destroy(); + }); + + test.each([ + ["best-effort closure", [createDelta("draft")], createRunTerminal("run.completed")], + ["run terminal closure", [createRunTerminal("run.failed")], createRunTerminal("run.completed")], + [ + "best-effort terminal", + [createEvent("message.completed")], + { ...createRunTerminal("run.completed"), delivery: "best_effort" }, + ], + ] as const)("rejects a %s before delivering any event", async (_name, closures, terminal) => { + let sends = 0; + const context = createContext({ + pushEvents: async () => { + sends += 1; + return { accepted: [] }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + + expect(() => publisher.pushTerminal(context, "terminal", closures, terminal)).toThrow(); + expect(sends).toBe(0); + await context.logger.destroy(); + }); + + test("rejects or drops events queued after a run terminal", async () => { + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const terminal = publisher.pushTerminal( + context, + "terminal", + [], + createRunTerminal("run.completed"), + ); + const lossless = publisher.push(context, "late-lossless", [createEvent("message.completed")]); + const bestEffort = publisher.push(context, "late-best-effort", [createDelta("late")]); + + await expect(lossless).rejects.toThrow("run terminal settlement slot is full"); + await expect(Promise.all([terminal, bestEffort])).resolves.toEqual([undefined, undefined]); + expect(kinds(attempts)).toEqual([["run.completed"]]); + await context.logger.destroy(); + }); + + test("requires the unique terminal entry point and an attributable run", async () => { + let sends = 0; + const context = createContext({ + currentRunId: () => null, + pushEvents: async () => { + sends += 1; + return { accepted: [] }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const { runId: _, ...unscoped } = createRunTerminal("run.completed"); + + await expect( + publisher.push(context, "terminal", [createRunTerminal("run.completed")]), + ).rejects.toThrow("must use pushTerminal"); + expect(() => publisher.pushTerminal(context, "terminal", [], unscoped)).toThrow( + "requires an active run", + ); + expect(sends).toBe(0); + await context.logger.destroy(); + }); + + test("settles sequential runs after the active run changes", async () => { + const attempts: DriverEventInput[][] = []; + let activeRunId: RunId | null = DRIVER_TEST_IDS.runId; + let seq = 0; + const context = createContext({ + currentRunId: () => activeRunId, + pushEvents: async (events) => { + attempts.push(events); + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + + await publisher.pushTerminal(context, "terminal.one", [], createRunTerminal("run.completed")); + activeRunId = null; + await publisher.push(context, "session.event", [createEvent("message.started")]); + activeRunId = DRIVER_TEST_IDS.secondRunId; + await publisher.pushTerminal(context, "terminal.two", [], { + ...createRunTerminal("run.completed"), + runId: DRIVER_TEST_IDS.secondRunId, + }); + + expect(attempts.map(([event]) => [event?.kind, event?.runId ?? null])).toEqual([ + ["run.completed", DRIVER_TEST_IDS.runId], + ["message.started", null], + ["run.completed", DRIVER_TEST_IDS.secondRunId], + ]); + await context.logger.destroy(); + }); + + test("keeps session events unscoped without reopening a settled run", async () => { + const attempts: DriverEventInput[][] = []; + let activeRunId: RunId | null = DRIVER_TEST_IDS.runId; + let seq = 0; + const context = createContext({ + currentRunId: () => activeRunId, + pushEvents: async (events) => { + const canonical = events.flatMap((event) => + toDriverEventEnvelopes(driverBootPayload, event, activeRunId), + ); + attempts.push(canonical.map(({ event }) => event)); + return { + accepted: canonical.map((envelope) => ({ + eventId: envelope.eventId, + seq: (seq += 1), + type: envelope.event.kind, + })), + }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const task = (taskId: string): DriverEventInput => ({ + kind: "agent.task.updated", + payload: { active: false, status: "completed", taskId }, + }); + + await publisher.pushTerminal(context, "terminal", [], createRunTerminal("run.completed")); + await publisher.pushSession(context, "session.same-run", [task("agent-1")]); + await expect( + publisher.push(context, "late-run", [createEvent("message.completed")]), + ).rejects.toThrow("run terminal settlement slot is full"); + activeRunId = DRIVER_TEST_IDS.secondRunId; + await publisher.pushSession(context, "session.next-run", [task("agent-2")]); + + expect(attempts.flat().map((event) => [event.kind, event.runId ?? null])).toEqual([ + ["run.completed", DRIVER_TEST_IDS.runId], + ["agent.task.updated", null], + ["agent.task.updated", null], + ]); + await context.logger.destroy(); + }); + + test("does not let another run displace an in-flight settlement", async () => { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + entered.resolve(); + await release.promise; + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const first = publisher.pushTerminal( + context, + "terminal.one", + [], + createRunTerminal("run.completed"), + ); + + await entered.promise; + expect(() => + publisher.pushTerminal(context, "terminal.two", [], { + ...createRunTerminal("run.completed"), + runId: DRIVER_TEST_IDS.secondRunId, + }), + ).toThrow("settlement slot is full"); + release.resolve(); + await first; + + expect(attempts).toHaveLength(1); + await context.logger.destroy(); + }); + + test("rejects an operation that targets a run other than the active run", async () => { + let sends = 0; + const context = createContext({ + currentRunId: () => DRIVER_TEST_IDS.runId, + pushEvents: async () => { + sends += 1; + return { accepted: [] }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + + expect(() => + publisher.pushTerminal(context, "terminal", [], { + ...createRunTerminal("run.completed"), + runId: DRIVER_TEST_IDS.secondRunId, + }), + ).toThrow("must target the active run"); expect(sends).toBe(0); await context.logger.destroy(); }); diff --git a/tests/driver-event-publisher-backpressure.test.ts b/tests/driver-event-publisher-backpressure.test.ts index 838dd8e..98c9357 100644 --- a/tests/driver-event-publisher-backpressure.test.ts +++ b/tests/driver-event-publisher-backpressure.test.ts @@ -1,13 +1,12 @@ import { describe, expect, test } from "bun:test"; -import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; import type { DriverEventBatchOutput } from "../src/protocol/orpc"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; -import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; +import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; import { bootPayload } from "./driver-runtime-boundary-fixtures"; function createTestLogger() { @@ -40,28 +39,6 @@ function createDelta(contentDelta: string): DriverEventInput { }; } -function createRunTerminal( - kind: "run.cancelled" | "run.completed" | "run.failed", -): DriverEventInput { - return { - kind, - payload: - kind === "run.failed" - ? { error: { code: "runtime_failed", message: "failed", retryable: false } } - : kind === "run.cancelled" - ? { requestedBy: "user", stopReason: "cancelled" } - : {}, - runId: DRIVER_TEST_IDS.runId, - }; -} - -function createUnscopedRunTerminal( - kind: "run.cancelled" | "run.completed" | "run.failed", -): DriverEventInput { - const { runId: _, ...event } = createRunTerminal(kind); - return event; -} - function kinds(batches: readonly (readonly DriverEventInput[])[]): string[][] { return batches.map((batch) => batch.map((event) => event.kind)); } @@ -73,7 +50,7 @@ function createContext(input: { return createAgentDriverContext({ eventSink: { commandUpdate: async () => {}, - ...(input.currentRunId === undefined ? {} : { currentRunId: input.currentRunId }), + currentRunId: input.currentRunId ?? (() => DRIVER_TEST_IDS.runId), pushEvents: async ({ events, signal }) => input.pushEvents(events, signal), }, logger: createTestLogger(), @@ -85,41 +62,6 @@ function createContext(input: { } describe("DriverEventPublisher", () => { - test("attributes a coalesced poison event only to its caller", async () => { - const delivered: DriverEventInput[] = []; - let nextSeq = 1; - const context = createContext({ - pushEvents: async (events) => { - const envelopes = events.flatMap((event) => - toDriverEventEnvelopes(driverBootPayload, event, DRIVER_TEST_IDS.runId), - ); - delivered.push(...envelopes.map(({ event }) => event)); - return { - accepted: envelopes.map((envelope) => ({ - eventId: envelope.eventId, - seq: nextSeq++, - type: envelope.event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const poison = publisher.push(context, "poison", [ - { kind: "invalid.kind", payload: {} } as unknown as DriverEventInput, - ]); - const terminal = publisher.push(context, "terminal", [createRunTerminal("run.completed")]); - - const outcomes = await Promise.allSettled([poison, terminal]); - - expect(outcomes[0]).toMatchObject({ - reason: expect.objectContaining({ message: expect.stringContaining("unsupported") }), - status: "rejected", - }); - expect(outcomes[1]).toEqual({ status: "fulfilled", value: undefined }); - expect(kinds([delivered])).toEqual([["run.completed"]]); - await context.logger.destroy(); - }); - test("resolves only fully accepted callers after a coalesced partial failure", async () => { const attempts: DriverEventInput[][] = []; const context = createContext({ @@ -153,305 +95,6 @@ describe("DriverEventPublisher", () => { await context.logger.destroy(); }); - test("treats an identical pending run terminal retry as one idempotent delivery", async () => { - const attempts: DriverEventInput[][] = []; - const context = createContext({ - pushEvents: async (events) => { - attempts.push(events); - - if (attempts.length === 1) { - throw new Error("socket unavailable"); - } - - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const terminal = createRunTerminal("run.completed"); - - await expect(publisher.push(context, "terminal", [terminal])).rejects.toThrow( - "socket unavailable", - ); - await expect(publisher.push(context, "terminal.retry", [terminal])).resolves.toBeUndefined(); - - expect(attempts).toHaveLength(2); - expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); - await context.logger.destroy(); - }); - - test("does not join the same raw terminal after the active run changes", async () => { - const attempts: DriverEventInput[][] = []; - const oldRunRecovered = Promise.withResolvers(); - let activeRunId = DRIVER_TEST_IDS.runId as RunId; - const context = createContext({ - currentRunId: () => activeRunId, - pushEvents: async (events) => { - attempts.push(events); - - if (attempts.length === 1) { - throw new Error("socket unavailable"); - } - - if (attempts.length === 2) { - oldRunRecovered.resolve(); - } - - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const terminal = createUnscopedRunTerminal("run.completed"); - - await expect(publisher.push(context, "run-1", [terminal])).rejects.toThrow( - "socket unavailable", - ); - activeRunId = DRIVER_TEST_IDS.secondRunId; - await expect(publisher.push(context, "run-2", [terminal])).rejects.toThrow("run terminal slot"); - await oldRunRecovered.promise; - await Bun.sleep(0); - await publisher.push(context, "run-2.retry", [terminal]); - - expect(attempts.map(([event]) => event?.runId)).toEqual([ - DRIVER_TEST_IDS.runId, - DRIVER_TEST_IDS.runId, - DRIVER_TEST_IDS.secondRunId, - ]); - expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); - expect(attempts[2]?.[0]?.sourceEventId).not.toBe(attempts[0]?.[0]?.sourceEventId); - await context.logger.destroy(); - }); - - test.each(["run.cancelled", "run.completed", "run.failed"] as const)( - "joins an old %s retry with an explicit frozen run after the active run changes", - async (kind) => { - const attempts: DriverEventInput[][] = []; - let activeRunId = DRIVER_TEST_IDS.runId as RunId; - const context = createContext({ - currentRunId: () => activeRunId, - pushEvents: async (events) => { - attempts.push(events); - - if (attempts.length === 1) { - throw new Error("socket unavailable"); - } - - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const terminal = createUnscopedRunTerminal(kind); - - await expect(publisher.push(context, "terminal", [terminal])).rejects.toThrow( - "socket unavailable", - ); - activeRunId = DRIVER_TEST_IDS.secondRunId; - await publisher.push(context, "terminal.retry", [ - { ...terminal, runId: DRIVER_TEST_IDS.runId }, - ]); - - expect(attempts.map(([event]) => event?.runId)).toEqual([ - DRIVER_TEST_IDS.runId, - DRIVER_TEST_IDS.runId, - ]); - expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); - await context.logger.destroy(); - }, - ); - - test.each([ - [ - "the same active run", - DRIVER_TEST_IDS.runId, - undefined, - DRIVER_TEST_IDS.runId, - DRIVER_TEST_IDS.runId, - ], - [ - "an explicit run override", - DRIVER_TEST_IDS.runId, - DRIVER_TEST_IDS.thirdRunId, - DRIVER_TEST_IDS.secondRunId, - DRIVER_TEST_IDS.thirdRunId, - ], - ["no active run", null, undefined, null, null], - ] as const)( - "keeps terminal retry identity with %s", - async (_name, initialActiveRunId, eventRunId, nextActiveRunId, expectedRunId) => { - const attempts: DriverEventInput[][] = []; - let activeRunId: RunId | null = initialActiveRunId; - const context = createContext({ - currentRunId: () => activeRunId, - pushEvents: async (events) => { - attempts.push(events); - - if (attempts.length === 1) { - throw new Error("socket unavailable"); - } - - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const raw = createUnscopedRunTerminal("run.completed"); - const terminal = eventRunId === undefined ? raw : { ...raw, runId: eventRunId }; - - await expect(publisher.push(context, "terminal", [terminal])).rejects.toThrow( - "socket unavailable", - ); - activeRunId = nextActiveRunId; - await publisher.push(context, "terminal.retry", [terminal]); - - expect(attempts.map(([event]) => event?.runId ?? null)).toEqual([ - expectedRunId, - expectedRunId, - ]); - expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); - await context.logger.destroy(); - }, - ); - - test.each([ - ["joins the pending terminal for the same", "source-terminal-1", true], - ["keeps the pending occurrence for a different", "source-terminal-2", false], - ] as const)("%s explicit source ID", async (_name, retrySourceEventId, joinsPending) => { - const attempts: DriverEventInput[][] = []; - const delivered = Promise.withResolvers(); - const context = createContext({ - currentRunId: () => DRIVER_TEST_IDS.runId, - pushEvents: async (events) => { - attempts.push(events); - - if (attempts.length === 1) { - throw new Error("socket unavailable"); - } - - delivered.resolve(); - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const terminal = { - ...createUnscopedRunTerminal("run.completed"), - sourceEventId: "source-terminal-1", - }; - - await expect(publisher.push(context, "terminal", [terminal])).rejects.toThrow( - "socket unavailable", - ); - const retry = publisher.push(context, "terminal.retry", [ - { ...terminal, sourceEventId: retrySourceEventId }, - ]); - - if (joinsPending) { - await expect(retry).resolves.toBeUndefined(); - } else { - await expect(retry).rejects.toThrow("run terminal slot"); - await delivered.promise; - await Bun.sleep(0); - } - - expect(attempts).toHaveLength(2); - expect(attempts.map(([event]) => event?.sourceEventId)).toEqual([ - "source-terminal-1", - "source-terminal-1", - ]); - await context.logger.destroy(); - }); - - test.each([ - ["ordinary lossless", "ordinary"], - ["different terminal", "terminal"], - ["best effort", "best_effort"], - ] as const)( - "a rejected or dropped %s push wakes a full retained terminal batch without joining it", - async (_name, triggerKind) => { - const attempts: DriverEventInput[][] = []; - const recovered = Promise.withResolvers(); - const context = createContext({ - pushEvents: async (events) => { - attempts.push(events); - - if (attempts.length <= 2) { - throw new Error(`socket unavailable ${attempts.length}`); - } - - recovered.resolve(); - return { - accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, - seq: index + 1, - type: event.kind, - })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const retained = Array.from({ length: 1_024 }, () => createEvent("message.started")); - - await expect(publisher.push(context, "fill", retained)).rejects.toThrow( - "socket unavailable 1", - ); - await expect( - publisher.push(context, "terminal", [createRunTerminal("run.completed")]), - ).rejects.toThrow("socket unavailable 2"); - - const trigger = - triggerKind === "ordinary" - ? publisher.push(context, "ordinary.retry", [createEvent("message.completed")]) - : triggerKind === "terminal" - ? publisher.push(context, "terminal.retry", [createRunTerminal("run.failed")]) - : publisher.push(context, "stream.retry", [createDelta("wake")]); - - if (triggerKind === "best_effort") { - await expect(trigger).resolves.toBeUndefined(); - } else { - await expect(trigger).rejects.toThrow( - triggerKind === "terminal" ? "run terminal slot" : "exceeds 1024 events", - ); - } - - expect( - await Promise.race([recovered.promise.then(() => true), Bun.sleep(50).then(() => false)]), - ).toBe(true); - expect(attempts).toHaveLength(3); - expect(attempts[2]?.map((event) => event.sourceEventId)).toEqual( - attempts[1]?.map((event) => event.sourceEventId), - ); - expect(attempts[2]?.at(-1)?.kind).toBe("run.completed"); - await context.logger.destroy(); - }, - ); - test.each([ ["rejected lossless", "lossless"], ["dropped best effort", "best_effort"], @@ -511,28 +154,4 @@ describe("DriverEventPublisher", () => { await context.logger.destroy(); }, ); - - test("does not spin while an explicitly retried pending terminal keeps failing", async () => { - let attempts = 0; - const context = createContext({ - currentRunId: () => DRIVER_TEST_IDS.runId, - pushEvents: async () => { - attempts += 1; - throw new Error("socket unavailable"); - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const terminal = createUnscopedRunTerminal("run.completed"); - - await expect(publisher.push(context, "terminal", [terminal])).rejects.toThrow( - "socket unavailable", - ); - await expect(publisher.push(context, "terminal.retry", [terminal])).rejects.toThrow( - "socket unavailable", - ); - await Bun.sleep(20); - - expect(attempts).toBe(2); - await context.logger.destroy(); - }); }); diff --git a/tests/driver-event-publisher-drain.test.ts b/tests/driver-event-publisher-drain.test.ts index 05888ba..6d90ea1 100644 --- a/tests/driver-event-publisher-drain.test.ts +++ b/tests/driver-event-publisher-drain.test.ts @@ -52,7 +52,7 @@ function createContext(input: { return createAgentDriverContext({ eventSink: { commandUpdate: async () => {}, - ...(input.currentRunId === undefined ? {} : { currentRunId: input.currentRunId }), + currentRunId: input.currentRunId ?? (() => DRIVER_TEST_IDS.runId), pushEvents: async ({ events, signal }) => input.pushEvents(events, signal), }, logger: createTestLogger(), diff --git a/tests/driver-golden-fixtures.test.ts b/tests/driver-golden-fixtures.test.ts index ab163ad..6d3bcd2 100644 --- a/tests/driver-golden-fixtures.test.ts +++ b/tests/driver-golden-fixtures.test.ts @@ -1,12 +1,14 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; -import { DRIVER_ID_FIXTURES } from "../src/protocol/boot/testing"; +import type { EventId } from "../src/protocol/id"; import { parseRuntimeCommand } from "../src/runtime-command"; import { ingestRuntimeEventInput } from "../src/runtime-events"; import type { RuntimeEventBuildContext } from "../src/runtime-events"; +import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; const occurredAt = "2026-05-26T00:00:00.000Z"; +const eventId = "01J0000000000000000000000G" as EventId; const commandFixtures = [ "input-start", @@ -31,12 +33,12 @@ function readJsonFixture(path: string): unknown { function createRuntimeEventContext(): RuntimeEventBuildContext { return { - createId: () => DRIVER_ID_FIXTURES.event, - driverInstanceId: DRIVER_ID_FIXTURES.driverInstance, + createId: () => eventId, + driverInstanceId: DRIVER_TEST_IDS.driverInstanceId, occurredAt, - runId: DRIVER_ID_FIXTURES.run, + runId: DRIVER_TEST_IDS.runId, runtimeId: "runtime-1", - sessionId: DRIVER_ID_FIXTURES.session, + sessionId: DRIVER_TEST_IDS.sessionId, traceId: "trace-1", }; } @@ -91,4 +93,52 @@ describe("Driver golden fixtures", () => { readJsonFixture(`./fixtures/driver/runtime-event-envelopes/${name}.json`), ); }); + + test("enforces the shared agent task payload contract", () => { + expect( + ingestRuntimeEventInput(createRuntimeEventContext(), { + kind: "agent.task.updated", + payload: { + active: true, + agentId: "agent-1", + status: "running", + taskId: "task-1", + taskType: "local_agent", + }, + }), + ).toMatchObject({ status: "accepted" }); + + for (const payload of [ + { status: "running" }, + { status: "unknown", taskId: "task-1" }, + { active: "yes", taskId: "task-1" }, + ]) { + expect( + ingestRuntimeEventInput(createRuntimeEventContext(), { + kind: "agent.task.updated", + payload, + }), + ).toMatchObject({ status: "rejected" }); + } + }); + + test("bounds control reasons without dropping their commands", () => { + const limit = "x".repeat(16 * 1_024); + const oversized = `${limit}界`; + const summary = "Runtime command reason exceeded 16384 UTF-8 bytes (received 16387)."; + + expect( + parseRuntimeCommand({ commandId: "cancel-at-limit", kind: "turn.cancel", reason: limit }), + ).toMatchObject({ reason: limit }); + expect( + parseRuntimeCommand({ + commandId: "cancel-oversized", + kind: "turn.cancel", + reason: oversized, + }), + ).toMatchObject({ kind: "turn.cancel", reason: summary }); + expect( + parseRuntimeCommand({ commandId: "stop-oversized", kind: "session.stop", reason: oversized }), + ).toMatchObject({ kind: "session.stop", reason: summary }); + }); }); diff --git a/tests/driver-permission-broker.test.ts b/tests/driver-permission-broker.test.ts index b702203..559d83c 100644 --- a/tests/driver-permission-broker.test.ts +++ b/tests/driver-permission-broker.test.ts @@ -5,9 +5,16 @@ import { PermissionEventDeliveryError, } from "../src/core/driver-permission-broker"; import type { DriverRuntimeEventPort } from "../src/core/driver-runtime-io"; +import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; import type { DriverEventInput } from "../src/protocol/events"; +import type { RunId } from "../src/protocol/id"; import type { DriverEventBatchOutput } from "../src/protocol/orpc"; +import { CMA_MAX_EVENT_BYTES } from "../src/stores/cma-store"; +import { createCmaMemoryStore } from "../src/stores/memory"; import { settlePromiseWithTimeout } from "../src/utils/async"; +import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; + +const runId = "run-1" as RunId; interface RecordingSocket extends DriverRuntimeEventPort { readonly pushedEvents: DriverEventInput[]; @@ -26,6 +33,7 @@ function createRecordingSocket(): RecordingSocket { const pushedEvents: DriverEventInput[] = []; return { + currentRunId: () => runId, pushedEvents, pushEvents: async (input) => { pushedEvents.push(...input.events); @@ -35,6 +43,15 @@ function createRecordingSocket(): RecordingSocket { } const permissionInput = { + agentId: "subagent-1", + blockedPath: "/workspace/secret", + decisionReason: "Path is outside the allowed roots.", + description: "Read access to /workspace/secret", + matchedAskRule: { + ruleContent: "Read(/workspace/secret/**)", + source: "project", + toolName: "Read", + }, rawInput: '{"command":"fd ."}', requestId: "permission-1", title: "Approve command execution", @@ -58,7 +75,16 @@ describe("DriverPermissionBroker", () => { { kind: "permission.requested", payload: { + agentId: "subagent-1", + blockedPath: "/workspace/secret", + decisionReason: "Path is outside the allowed roots.", + description: "Read access to /workspace/secret", details: '{"command":"fd ."}', + matchedAskRule: { + ruleContent: "Read(/workspace/secret/**)", + source: "project", + toolName: "Read", + }, requestId: "permission-1", targetItemId: "tool-1", title: "Approve command execution", @@ -153,6 +179,55 @@ describe("DriverPermissionBroker", () => { expect(socket.pushedEvents).toEqual([]); }); + test("rejects an oversized permission event before the real CMA boundary", async () => { + const broker = new DriverPermissionBroker(() => null); + const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); + const pushedEvents: DriverEventInput[] = []; + let sequence = 0; + const socket: RecordingSocket = { + currentRunId: () => DRIVER_TEST_IDS.runId, + pushedEvents, + pushEvents: async ({ events }) => { + for (const event of events) { + const [envelope] = toDriverEventEnvelopes( + driverBootPayload, + event, + DRIVER_TEST_IDS.runId, + ); + await store.appendDriverEvent(DRIVER_TEST_IDS.sessionId, envelope!.event); + } + pushedEvents.push(...events); + return { + accepted: events.map((event) => ({ + eventId: event.sourceEventId, + seq: (sequence += 1), + type: event.kind, + })), + }; + }, + }; + const safeInput = { ...permissionInput, rawInput: "x".repeat(500_000) }; + const safeRequest = broker.request(socket, safeInput); + + await Bun.sleep(0); + expect(broker.resolve(safeInput.requestId, "reject_once")).toBe(true); + await expect(safeRequest).resolves.toBe("reject_once"); + expect(pushedEvents.map(({ kind }) => kind)).toEqual([ + "permission.requested", + "permission.resolved", + ]); + + await expect( + broker.request(socket, { + ...permissionInput, + rawInput: "x".repeat(CMA_MAX_EVENT_BYTES), + requestId: "permission-oversized", + }), + ).rejects.toThrow("permission request event exceeds 524288 UTF-8 bytes"); + expect(pushedEvents).toHaveLength(2); + expect(broker.hasPending()).toBe(false); + }); + test.each([0, -1, 1.5, Number.POSITIVE_INFINITY, Number.NaN])( "rejects invalid pending limits %p", (limit) => { @@ -187,6 +262,7 @@ describe("DriverPermissionBroker", () => { const controller = new AbortController(); let failPublish = mode === "publish failure"; const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { if (failPublish) { failPublish = false; @@ -241,6 +317,7 @@ describe("DriverPermissionBroker", () => { let stallRequested = true; let requestedEvents: readonly DriverEventInput[] = []; const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { if (stallRequested && events.some((event) => event.kind === "permission.requested")) { requestedEvents = events; @@ -303,6 +380,7 @@ describe("DriverPermissionBroker", () => { let stallResolution = true; let resolutionEvents: readonly DriverEventInput[] = []; const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { if (stallResolution && events.some((event) => event.kind === "permission.resolved")) { resolutionEvents = events; @@ -363,6 +441,7 @@ describe("DriverPermissionBroker", () => { requestTimeoutMs: 1, }); const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { if (events.some((event) => event.kind === "permission.resolved")) { await Bun.sleep(10); @@ -383,6 +462,7 @@ describe("DriverPermissionBroker", () => { maxPendingRequests: 1, }); const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { if (failResolution && events.some((event) => event.kind === "permission.resolved")) { throw deliveryFailure; @@ -435,6 +515,7 @@ describe("DriverPermissionBroker", () => { const releaseResolution = Promise.withResolvers(); const controller = new AbortController(); const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { if (events.some((event) => event.kind === "permission.resolved")) { resolutionPublishing.resolve(); @@ -493,6 +574,7 @@ describe("DriverPermissionBroker", () => { let resolvedEvents = 0; const broker = new DriverPermissionBroker(() => null, { requestTimeoutMs: 100 }); const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { if (events.some((event) => event.kind === "permission.resolved")) { resolvedEvents += 1; @@ -553,6 +635,7 @@ describe("DriverPermissionBroker", () => { requestTimeoutMs: 100, }); const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { if (events.some((event) => event.kind === "permission.resolved")) { resolutionCount += 1; @@ -586,6 +669,7 @@ describe("DriverPermissionBroker", () => { let fail = true; const broker = new DriverPermissionBroker(() => null); const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { if (fail) { fail = false; @@ -669,6 +753,7 @@ describe("DriverPermissionBroker", () => { const releaseResolution = Promise.withResolvers(); const pushedEvents: DriverEventInput[] = []; const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { pushedEvents.push(...events); @@ -723,6 +808,7 @@ describe("DriverPermissionBroker", () => { const requestedPublishing = Promise.withResolvers(); const pushedKinds: string[] = []; const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events, signal }) => { pushedKinds.push(...events.map((event) => event.kind)); @@ -759,6 +845,7 @@ describe("DriverPermissionBroker", () => { const acceptedKinds: string[] = []; const pushedKinds: string[] = []; const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events, signal }) => { pushedKinds.push(...events.map((event) => event.kind)); @@ -799,6 +886,7 @@ describe("DriverPermissionBroker", () => { const controller = new AbortController(); let deliveryAborted = false; const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events, signal }) => { if (events.some((event) => event.kind === "permission.resolved")) { resolutionPublishing.resolve(); @@ -886,6 +974,7 @@ describe("DriverPermissionBroker", () => { let pushCount = 0; const broker = new DriverPermissionBroker(() => null, { requestTimeoutMs: 1 }); const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { pushCount += 1; @@ -913,6 +1002,7 @@ describe("DriverPermissionBroker", () => { const batchSizes: number[] = []; const broker = new DriverPermissionBroker(() => null); const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { batchSizes.push(events.length); return acceptEvents(events.slice(0, 1)); @@ -930,6 +1020,7 @@ describe("DriverPermissionBroker", () => { test("fails and releases a request when event delivery makes no progress", async () => { const broker = new DriverPermissionBroker(() => null); const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async () => ({ accepted: [] }), }; @@ -954,6 +1045,7 @@ describe("DriverPermissionBroker", () => { const broker = new DriverPermissionBroker(() => null); const pushedEvents: DriverEventInput[] = []; const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, pushEvents: async ({ events }) => { pushedEvents.push(...events); diff --git a/tests/driver-permission-policy.test.ts b/tests/driver-permission-policy.test.ts index 2ce1325..8c65ba0 100644 --- a/tests/driver-permission-policy.test.ts +++ b/tests/driver-permission-policy.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import type { DriverPermissionRequest } from "../src/core/driver-permission-broker"; +import type { DriverPermissionRequest } from "../src/host-ports"; import { createDriverPermissionRequestHandler, isDriverFullAccess, @@ -76,6 +76,28 @@ describe("driver permission policy", () => { expect(supervisedCalls).toBe(0); }); + test("full_access preserves a user-configured ask rule", async () => { + const seen: DriverPermissionRequest[] = []; + const handler = createDriverPermissionRequestHandler({ + payload: startInputWithPolicy("full_access"), + supervised: async (request) => { + seen.push(request); + return "reject_once"; + }, + }); + const request = { + ...SAMPLE_REQUEST, + matchedAskRule: { + ruleContent: "Bash(*)", + source: "project", + toolName: "Bash", + }, + }; + + await expect(handler(request)).resolves.toBe("reject_once"); + expect(seen).toEqual([request]); + }); + test("supervised delegates to the interactive handler", async () => { const seen: DriverPermissionRequest[] = []; const controller = new AbortController(); diff --git a/tests/driver-runtime-boundary-fixtures.ts b/tests/driver-runtime-boundary-fixtures.ts index 2f6dbc5..7b961ee 100644 --- a/tests/driver-runtime-boundary-fixtures.ts +++ b/tests/driver-runtime-boundary-fixtures.ts @@ -20,18 +20,25 @@ export class FakeDriverRuntimeIo implements DriverRuntimeIo { readonly pushedEvents: Parameters[0][] = []; readonly updates: Parameters[0][] = []; readonly #commands: readonly RuntimeCommand[]; + #activeRunId: ReturnType = null; #commandIndex = 0; constructor(commands: readonly RuntimeCommand[]) { this.#commands = commands; } - beginRun(): void { - return; + beginRun(runId: Parameters[0]): void { + this.#activeRunId = runId; } - endRun(): void { - return; + currentRunId(): ReturnType { + return this.#activeRunId; + } + + endRun(runId: Parameters[0]): void { + if (this.#activeRunId === runId) { + this.#activeRunId = null; + } } async heartbeat(): ReturnType { diff --git a/tests/driver-runtime-boundary-state.test.ts b/tests/driver-runtime-boundary-state.test.ts index d1deebe..a0ebf8e 100644 --- a/tests/driver-runtime-boundary-state.test.ts +++ b/tests/driver-runtime-boundary-state.test.ts @@ -147,6 +147,20 @@ describe("driver runtime boundary", () => { expect(event?.event.runId).toBe(DRIVER_TEST_IDS.thirdRunId); }); + test("driver socket preserves explicit session scope during an active turn", () => { + const [event] = toDriverEventEnvelopes( + driverBootPayload, + { + kind: "agent.task.updated", + payload: { active: false, status: "completed", taskId: "agent-1" }, + runId: null, + }, + DRIVER_TEST_IDS.secondRunId, + ); + + expect(event?.event.runId).toBeUndefined(); + }); + test("driver socket preserves a valid explicit run id during another active turn", () => { const draft = { kind: "run.started", diff --git a/tests/driver-runtime-boundary-timing.test.ts b/tests/driver-runtime-boundary-timing.test.ts index 122122b..bc94956 100644 --- a/tests/driver-runtime-boundary-timing.test.ts +++ b/tests/driver-runtime-boundary-timing.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from "bun:test"; +import { ACTIVE_TURN_CANCEL_GRACE_MS } from "../src/core/driver-command-dispatcher"; +import { DRIVER_EVENT_DELIVERY_TIMEOUT_MS } from "../src/core/driver-runtime-io"; import { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; import type { RuntimeCommand } from "../src/runtime-command"; import { settlePromiseWithTimeout } from "../src/utils/async"; @@ -12,6 +14,91 @@ import { } from "./driver-runtime-boundary-fixtures"; describe("driver runtime boundary", () => { + test("allows cancellation cleanup to consume the public event delivery deadline", async () => { + const backend = createBackend(); + const inputEntered = Promise.withResolvers(); + const runtimeState = new DriverRuntimeStateMachine("ready"); + const socket = new FakeDriverRuntimeIo([ + { + commandId: "input-with-lossless-terminal", + input: { text: "wait" }, + kind: "input.start", + requestId: "request-with-lossless-terminal", + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: "cancel-with-lossless-terminal", + kind: "turn.cancel", + reason: "test.cancel", + }, + ]); + const nativeSetTimeout = globalThis.setTimeout; + const delay = (milliseconds: number) => + new Promise((resolve) => nativeSetTimeout(resolve, milliseconds)); + const recordEvents = socket.pushEvents.bind(socket); + socket.pushEvents = async (input) => { + await delay(90); + return recordEvents(input); + }; + backend.handleInput = async (context, _input, runId, signal) => { + inputEntered.resolve(); + await new Promise((resolve) => { + signal!.addEventListener("abort", () => nativeSetTimeout(resolve, 30), { once: true }); + }); + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.cancelled", + payload: { reason: "test.cancel" }, + runId, + }, + ], + }); + }; + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => + socket.updates.some( + (update) => + update.commandId === "cancel-with-lossless-terminal" && update.status === "completed", + ), + runtimeState, + }); + const acceleratedSetTimeout = ( + callback: (...args: unknown[]) => void, + timeout?: number, + ...arguments_: unknown[] + ) => + nativeSetTimeout( + callback, + timeout === 5_000 + ? 20 + : timeout === DRIVER_EVENT_DELIVERY_TIMEOUT_MS + ? 100 + : timeout === ACTIVE_TURN_CANCEL_GRACE_MS + DRIVER_EVENT_DELIVERY_TIMEOUT_MS + ? 200 + : timeout, + ...arguments_, + ); + globalThis.setTimeout = acceleratedSetTimeout as typeof setTimeout; + + try { + const run = dispatcher.run(socket, logger); + await inputEntered.promise; + await expect(run).resolves.toBeUndefined(); + expect(runtimeState.status()).toBe("ready"); + expect(socket.failedRuns).toEqual([]); + expect(socket.pushedEvents).toMatchObject([{ events: [{ kind: "run.cancelled" }] }]); + expect(socket.updates).toContainEqual({ + commandId: "cancel-with-lossless-terminal", + status: "completed", + }); + } finally { + globalThis.setTimeout = nativeSetTimeout; + await logger.destroy(); + } + }); + test("external shutdown cancels local input and command polling immediately", async () => { const entered = Promise.withResolvers(); const release = Promise.withResolvers(); diff --git a/tests/fixtures/architecture-boundaries-audit.ts b/tests/fixtures/architecture-boundaries-audit.ts new file mode 100644 index 0000000..c05740f --- /dev/null +++ b/tests/fixtures/architecture-boundaries-audit.ts @@ -0,0 +1,22 @@ +declare const moduleName: string; + +void import("./" + moduleName); +void require("node:fs"); +void ((load) => load("node:fs"))(require); +void import.meta.require("node:fs"); +void import.meta["require"]("node:fs"); +void module.exports; +void exports.value; +void require; +void module; +void exports; +void process.getBuiltinModule("node:module"); +void process["getBuiltinModule"]("module"); + +// @ts-expect-error -- Exercises the CommonJS syntax guard. +import legacy = require("node:fs"); +// @ts-expect-error -- Exercises the CommonJS syntax guard. +export = legacy; + +import "node:module"; +import "module"; diff --git a/tests/fixtures/architecture-boundaries-resolution.ts b/tests/fixtures/architecture-boundaries-resolution.ts new file mode 100644 index 0000000..a634aaa --- /dev/null +++ b/tests/fixtures/architecture-boundaries-resolution.ts @@ -0,0 +1,25 @@ +// oxlint-disable-next-line typescript/triple-slash-reference -- Exercises dependency auditing. +/// + +import packageMetadata from "../../package.json" with { type: "json" }; +import "../../src/paths.js"; +import type { Module } from "node:module"; + +export type { Command } from "../../src/contract/index.js"; +export type { Module as ExportedNodeModule } from "node:module"; +export type ImportedNodeModule = Module; +export const moduleUrl = import.meta.url; + +const require = () => packageMetadata.name; +const module = packageMetadata.name; +const exports = packageMetadata.name; +export const process = { getBuiltinModule: (_name: string) => packageMetadata.name }; + +void packageMetadata; +void require(); +void module; +void exports; +void process.getBuiltinModule("node:module"); +void { exports: packageMetadata.name, module, require }; + +declare module "../../src/core/agent-driver-kernel.ts" {} diff --git a/tests/fixtures/architecture-boundaries-runtime-loaders.ts b/tests/fixtures/architecture-boundaries-runtime-loaders.ts new file mode 100644 index 0000000..9be9de9 --- /dev/null +++ b/tests/fixtures/architecture-boundaries-runtime-loaders.ts @@ -0,0 +1,26 @@ +import { getBuiltinModule as importedLoader } from "node:process"; + +import { process as localProcess } from "./architecture-boundaries-resolution.js"; + +const { getBuiltinModule: destructuredLoader } = process; +const builtinModuleKey = "getBuiltinModule" as const; + +export function direct(): void { + void process.getBuiltinModule("node:module"); +} + +export function imported(): void { + void importedLoader("node:module"); +} + +export function destructured(): void { + void destructuredLoader("node:module"); +} + +export function computed(): void { + void process[builtinModuleKey]("node:module"); +} + +export function local(): void { + void localProcess.getBuiltinModule("node:module"); +} diff --git a/tests/fixtures/driver/runtime-event-envelopes/diagnostic-reported.json b/tests/fixtures/driver/runtime-event-envelopes/diagnostic-reported.json index 21c0abd..2ebc25c 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/diagnostic-reported.json +++ b/tests/fixtures/driver/runtime-event-envelopes/diagnostic-reported.json @@ -1,7 +1,7 @@ { "actor": "driver", "delivery": "lossless", - "driverInstanceId": "01J00000000000000000000008", + "driverInstanceId": "01J0000000000000000000000F", "id": "01J0000000000000000000000G", "kind": "diagnostic.reported", "occurredAt": "2026-05-26T00:00:00.000Z", @@ -11,10 +11,10 @@ "message": "Remote MCP execute failed.", "severity": "error" }, - "runId": "01J0000000000000000000000N", + "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", "schemaVersion": "2026-05-26", - "sessionId": "01J0000000000000000000000K", + "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "owner_debug" } diff --git a/tests/fixtures/driver/runtime-event-envelopes/message-delta.json b/tests/fixtures/driver/runtime-event-envelopes/message-delta.json index d17ebea..83f3d93 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/message-delta.json +++ b/tests/fixtures/driver/runtime-event-envelopes/message-delta.json @@ -1,7 +1,7 @@ { "actor": "driver", "delivery": "lossless", - "driverInstanceId": "01J00000000000000000000008", + "driverInstanceId": "01J0000000000000000000000F", "id": "01J0000000000000000000000G", "kind": "message.delta", "occurredAt": "2026-05-26T00:00:00.000Z", @@ -11,10 +11,10 @@ "messageId": "message-1", "role": "agent" }, - "runId": "01J0000000000000000000000N", + "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", "schemaVersion": "2026-05-26", - "sessionId": "01J0000000000000000000000K", + "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" } diff --git a/tests/fixtures/driver/runtime-event-envelopes/permission-requested.json b/tests/fixtures/driver/runtime-event-envelopes/permission-requested.json index cd4ba67..d611748 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/permission-requested.json +++ b/tests/fixtures/driver/runtime-event-envelopes/permission-requested.json @@ -1,7 +1,7 @@ { "actor": "driver", "delivery": "lossless", - "driverInstanceId": "01J00000000000000000000008", + "driverInstanceId": "01J0000000000000000000000F", "id": "01J0000000000000000000000G", "kind": "permission.requested", "occurredAt": "2026-05-26T00:00:00.000Z", @@ -17,10 +17,10 @@ "toolCallId": "tool-1" } }, - "runId": "01J0000000000000000000000N", + "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", "schemaVersion": "2026-05-26", - "sessionId": "01J0000000000000000000000K", + "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" } diff --git a/tests/fixtures/driver/runtime-event-envelopes/run-started.json b/tests/fixtures/driver/runtime-event-envelopes/run-started.json index c774b4d..279108d 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/run-started.json +++ b/tests/fixtures/driver/runtime-event-envelopes/run-started.json @@ -1,7 +1,7 @@ { "actor": "driver", "delivery": "lossless", - "driverInstanceId": "01J00000000000000000000008", + "driverInstanceId": "01J0000000000000000000000F", "id": "01J0000000000000000000000G", "kind": "run.started", "occurredAt": "2026-05-26T00:00:00.000Z", @@ -12,10 +12,10 @@ "startedAt": "2026-05-26T00:00:00.000Z", "status": "running" }, - "runId": "01J0000000000000000000000N", + "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", "schemaVersion": "2026-05-26", - "sessionId": "01J0000000000000000000000K", + "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" } diff --git a/tests/fixtures/driver/runtime-event-envelopes/tool-call-updated.json b/tests/fixtures/driver/runtime-event-envelopes/tool-call-updated.json index d0ba049..66efdfd 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/tool-call-updated.json +++ b/tests/fixtures/driver/runtime-event-envelopes/tool-call-updated.json @@ -1,7 +1,7 @@ { "actor": "driver", "delivery": "lossless", - "driverInstanceId": "01J00000000000000000000008", + "driverInstanceId": "01J0000000000000000000000F", "id": "01J0000000000000000000000G", "kind": "tool.call.updated", "occurredAt": "2026-05-26T00:00:00.000Z", @@ -16,10 +16,10 @@ "title": "Run shell command", "toolCallId": "tool-1" }, - "runId": "01J0000000000000000000000N", + "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", "schemaVersion": "2026-05-26", - "sessionId": "01J0000000000000000000000K", + "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" } diff --git a/tests/fixtures/driver/runtime-event-envelopes/usage-updated.json b/tests/fixtures/driver/runtime-event-envelopes/usage-updated.json index cbcec36..671ac74 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/usage-updated.json +++ b/tests/fixtures/driver/runtime-event-envelopes/usage-updated.json @@ -1,7 +1,7 @@ { "actor": "driver", "delivery": "lossless", - "driverInstanceId": "01J00000000000000000000008", + "driverInstanceId": "01J0000000000000000000000F", "id": "01J0000000000000000000000G", "kind": "usage.updated", "occurredAt": "2026-05-26T00:00:00.000Z", @@ -11,10 +11,10 @@ "model": "model-1", "outputTokens": 3 }, - "runId": "01J0000000000000000000000N", + "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", "schemaVersion": "2026-05-26", - "sessionId": "01J0000000000000000000000K", + "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" } diff --git a/tests/fixtures/providers/acp/cases/max-turn-failure.json b/tests/fixtures/providers/acp/cases/max-turn-failure.json index e6c97a0..1a6edca 100644 --- a/tests/fixtures/providers/acp/cases/max-turn-failure.json +++ b/tests/fixtures/providers/acp/cases/max-turn-failure.json @@ -1,8 +1,7 @@ { "begin": { "messageId": "message-1", - "runId": "run-1", - "sessionId": "session-1" + "runId": "run-1" }, "updates": [ { @@ -47,23 +46,27 @@ "parentMessageId": "assistant-message-1" }, "runId": "run-1", - "sourceEventId": "acp:session-1:run-1:tool-call:1" + "sourceEventId": "acp:run-1:tool-call:1" }, { - "kind": "message.completed", + "kind": "message.failed", "payload": { + "error": { + "code": "acp.max_turn_requests", + "message": "ACP prompt stopped with max_turn_requests.", + "retryable": false + }, "messageId": "assistant-message-1", - "role": "agent" + "role": "agent", + "stopReason": "max_turn_requests" }, "runId": "run-1" }, { "kind": "tool.call.updated", "payload": { - "kind": "tool", - "parentMessageId": "assistant-message-1", - "status": "completed", - "title": "Run command", + "error": "ACP prompt stopped with max_turn_requests.", + "status": "failed", "toolCallId": "tool-1" }, "runId": "run-1" @@ -71,15 +74,21 @@ { "kind": "item.completed", "payload": { + "error": "ACP prompt stopped with max_turn_requests.", "itemId": "tool-1", "itemType": "tool_call", - "status": "completed" + "status": "failed" }, "runId": "run-1" }, { - "kind": "run.completed", + "kind": "run.failed", "payload": { + "error": { + "code": "acp.max_turn_requests", + "message": "ACP prompt stopped with max_turn_requests." + }, + "recoverable": false, "stopReason": "max_turn_requests" }, "runId": "run-1" diff --git a/tests/fixtures/providers/acp/cases/permission-request.json b/tests/fixtures/providers/acp/cases/permission-request.json index 485f65f..8568a9c 100644 --- a/tests/fixtures/providers/acp/cases/permission-request.json +++ b/tests/fixtures/providers/acp/cases/permission-request.json @@ -1,8 +1,7 @@ { "begin": { "messageId": "message-1", - "runId": "run-1", - "sessionId": "session-1" + "runId": "run-1" }, "permissionRequest": { "params": { @@ -63,36 +62,6 @@ }, "runId": "run-1" }, - { - "kind": "permission.requested", - "payload": { - "defaultOptionId": "allow", - "details": "{\"command\":\"pwd\"}", - "options": [ - { - "kind": "allow_once", - "name": "Allow once", - "optionId": "allow" - }, - { - "kind": "reject_once", - "name": "Reject once", - "optionId": "reject" - } - ], - "requestId": "rpc-42", - "targetItemId": "tool-1", - "title": "Run command", - "toolCall": { - "kind": "shell", - "rawInput": "{\"command\":\"pwd\"}", - "status": "running", - "title": "Run command", - "toolCallId": "tool-1" - } - }, - "runId": "run-1" - }, { "kind": "message.completed", "payload": { @@ -104,10 +73,7 @@ { "kind": "tool.call.updated", "payload": { - "kind": "shell", - "rawInput": "{\"command\":\"pwd\"}", "status": "completed", - "title": "Run command", "toolCallId": "tool-1" }, "runId": "run-1" diff --git a/tests/fixtures/providers/acp/cases/session-ready.json b/tests/fixtures/providers/acp/cases/session-ready.json index 9c27bd4..b743585 100644 --- a/tests/fixtures/providers/acp/cases/session-ready.json +++ b/tests/fixtures/providers/acp/cases/session-ready.json @@ -3,24 +3,21 @@ "mode": "created", "nativeSessionId": "native-session-1", "setup": { - "availableModes": [ - { - "id": "default", - "name": "Default" - } - ], - "capabilities": { - "fileSystem": true - }, "configOptions": [ { "id": "approval", "type": "select" } ], - "currentModeId": "default", - "currentModel": "sonnet", - "models": ["sonnet"] + "modes": { + "availableModes": [ + { + "id": "default", + "name": "Default" + } + ], + "currentModeId": "default" + } } }, "expectedEvents": [ @@ -39,6 +36,7 @@ "visibility": "owner_debug" }, { + "delivery": "best_effort", "kind": "session.mode.updated", "payload": { "availableModes": [ @@ -51,13 +49,7 @@ } }, { - "kind": "session.models.updated", - "payload": { - "availableModels": ["sonnet"], - "currentModel": "sonnet" - } - }, - { + "delivery": "best_effort", "kind": "session.config.updated", "payload": { "options": [ @@ -67,15 +59,6 @@ } ] } - }, - { - "kind": "session.capabilities.updated", - "payload": { - "capabilities": { - "fileSystem": true - } - }, - "visibility": "owner_debug" } ] } diff --git a/tests/fixtures/providers/acp/cases/thought-and-unknown-update.json b/tests/fixtures/providers/acp/cases/thought-and-unknown-update.json index 0def5ba..b34df05 100644 --- a/tests/fixtures/providers/acp/cases/thought-and-unknown-update.json +++ b/tests/fixtures/providers/acp/cases/thought-and-unknown-update.json @@ -1,8 +1,7 @@ { "begin": { "messageId": "message-1", - "runId": "run-1", - "sessionId": "session-1" + "runId": "run-1" }, "updates": [ { @@ -47,9 +46,10 @@ "thoughtId": "message-1:thought" }, "runId": "run-1", - "sourceEventId": "acp:session-1:run-1:agent-thought:1" + "sourceEventId": "acp:run-1:agent-thought:1" }, { + "delivery": "best_effort", "kind": "diagnostic.reported", "payload": { "message": "Unsupported ACP session update: mystery_update.", diff --git a/tests/fixtures/providers/acp/cases/turn-text-tool-usage.json b/tests/fixtures/providers/acp/cases/turn-text-tool-usage.json index 6413148..6a18333 100644 --- a/tests/fixtures/providers/acp/cases/turn-text-tool-usage.json +++ b/tests/fixtures/providers/acp/cases/turn-text-tool-usage.json @@ -1,8 +1,7 @@ { "begin": { "messageId": "message-1", - "runId": "run-1", - "sessionId": "session-1" + "runId": "run-1" }, "updates": [ { @@ -79,7 +78,7 @@ "role": "agent" }, "runId": "run-1", - "sourceEventId": "acp:session-1:run-1:agent-message:1" + "sourceEventId": "acp:run-1:agent-message:1" }, { "kind": "item.started", @@ -102,7 +101,7 @@ "parentMessageId": "assistant-message-1" }, "runId": "run-1", - "sourceEventId": "acp:session-1:run-1:tool-call:2" + "sourceEventId": "acp:run-1:tool-call:2" }, { "delivery": "lossless", @@ -117,20 +116,27 @@ "rawOutput": "{\"text\":\"/workspace\"}" }, "runId": "run-1", - "sourceEventId": "acp:session-1:run-1:tool-call-update:3" + "sourceEventId": "acp:run-1:tool-call-update:3" }, { "kind": "item.completed", "payload": { "itemId": "tool-1", "itemType": "tool_call", - "result": { - "text": "/workspace" - }, "status": "completed" }, "runId": "run-1" }, + { + "delivery": "lossless", + "kind": "message.added", + "payload": { + "content": "hello", + "messageId": "assistant-message-1", + "role": "agent" + }, + "runId": "run-1" + }, { "kind": "message.completed", "payload": { diff --git a/tests/fixtures/providers/claude-agent-sdk/cases/result-failure-diagnostic.json b/tests/fixtures/providers/claude-agent-sdk/cases/result-failure-diagnostic.json index b9a025b..05908f3 100644 --- a/tests/fixtures/providers/claude-agent-sdk/cases/result-failure-diagnostic.json +++ b/tests/fixtures/providers/claude-agent-sdk/cases/result-failure-diagnostic.json @@ -7,18 +7,41 @@ "type": "rate_limit_event" }, { + "duration_api_ms": 8, + "duration_ms": 10, "errors": ["too many turns"], + "is_error": true, + "modelUsage": { + "claude-sonnet-4-6": { + "cacheCreationInputTokens": 0, + "cacheReadInputTokens": 0, + "contextWindow": 200000, + "costUSD": 0, + "inputTokens": 1, + "maxOutputTokens": 64000, + "outputTokens": 0, + "webSearchRequests": 0 + } + }, + "num_turns": 1, + "permission_denials": [], + "session_id": "claude-session-2", + "stop_reason": null, "subtype": "error_max_turns", - "total_cost_usd": null, + "terminal_reason": "max_turns", + "total_cost_usd": 0, "type": "result", "usage": { - "input_tokens": 1 - } + "input_tokens": 1, + "output_tokens": 0 + }, + "uuid": "result-1" } ], - "expectedNativeSessionIds": ["claude-session-2"], + "expectedNativeSessionIds": ["claude-session-2", "claude-session-2"], "expectedEvents": [ { + "delivery": "best_effort", "kind": "diagnostic.reported", "payload": { "message": "driver.claude.diagnostic", @@ -35,12 +58,12 @@ { "kind": "usage.updated", "payload": { - "cachedReadTokens": null, - "cachedWriteTokens": null, - "costAmount": null, - "costCurrency": null, + "cachedReadTokens": 0, + "cachedWriteTokens": 0, + "costAmount": 0, + "costCurrency": "USD", "inputTokens": 1, - "outputTokens": null, + "outputTokens": 0, "size": null, "source": "session_update", "thoughtTokens": null, @@ -54,7 +77,11 @@ "payload": { "error": { "code": "claude.error_max_turns", - "message": "too many turns" + "details": { + "terminalReason": "max_turns" + }, + "message": "too many turns", + "retryable": false }, "recoverable": false }, diff --git a/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json b/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json index cae662c..74b6c08 100644 --- a/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json +++ b/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json @@ -78,16 +78,37 @@ "type": "stream_event" }, { + "duration_api_ms": 8, + "duration_ms": 10, + "is_error": false, + "modelUsage": { + "claude-sonnet-4-6": { + "cacheCreationInputTokens": 0, + "cacheReadInputTokens": 0, + "contextWindow": 200000, + "costUSD": 0.02, + "inputTokens": 3, + "maxOutputTokens": 64000, + "outputTokens": 5, + "webSearchRequests": 0 + } + }, + "num_turns": 1, + "permission_denials": [], "result": "hello", + "session_id": "claude-session-1", + "stop_reason": "end_turn", "subtype": "success", "total_cost_usd": 0.02, "type": "result", "usage": { "input_tokens": 3, "output_tokens": 5 - } + }, + "uuid": "result-1" } ], + "expectedNativeSessionIds": ["claude-session-1"], "expectedEvents": [ { "kind": "message.started", @@ -163,7 +184,6 @@ "payload": { "content": "[{\"text\":\"/workspace\",\"type\":\"text\"}]", "messageId": "", - "rawOutput": "[{\"text\":\"/workspace\",\"type\":\"text\"}]", "status": "completed", "toolCallId": "tool-1" } @@ -183,18 +203,11 @@ "thoughtId": ":thought" } }, - { - "kind": "message.completed", - "payload": { - "messageId": "", - "role": "agent" - } - }, { "kind": "usage.updated", "payload": { - "cachedReadTokens": null, - "cachedWriteTokens": null, + "cachedReadTokens": 0, + "cachedWriteTokens": 0, "costAmount": 0.02, "costCurrency": "USD", "inputTokens": 3, @@ -207,6 +220,13 @@ "used": null } }, + { + "kind": "message.completed", + "payload": { + "messageId": "", + "role": "agent" + } + }, { "kind": "run.completed", "payload": { diff --git a/tests/fixtures/providers/claude-agent-sdk/cases/system-files-and-session.json b/tests/fixtures/providers/claude-agent-sdk/cases/system-files-and-session.json index aad64ac..b85d684 100644 --- a/tests/fixtures/providers/claude-agent-sdk/cases/system-files-and-session.json +++ b/tests/fixtures/providers/claude-agent-sdk/cases/system-files-and-session.json @@ -28,7 +28,7 @@ "type": "system" } ], - "expectedNativeSessionIds": ["claude-session-1", "claude-session-1"], + "expectedNativeSessionIds": ["claude-session-1"], "expectedEvents": [ { "kind": "session.info.updated", @@ -51,9 +51,11 @@ } }, { + "delivery": "best_effort", "kind": "diagnostic.reported", "payload": { - "failed": ["src/b.ts"], + "failedCount": 1, + "failedUtf8Bytes": 12, "message": "Claude file persistence failed.", "severity": "warn" }, diff --git a/tests/fixtures/providers/claude-agent-sdk/cases/unknown-message-diagnostic.json b/tests/fixtures/providers/claude-agent-sdk/cases/unknown-message-diagnostic.json new file mode 100644 index 0000000..060533f --- /dev/null +++ b/tests/fixtures/providers/claude-agent-sdk/cases/unknown-message-diagnostic.json @@ -0,0 +1,24 @@ +{ + "runId": "run-1", + "messages": [ + { + "type": "future_event", + "value": 123 + } + ], + "expectedEvents": [ + { + "delivery": "best_effort", + "kind": "diagnostic.reported", + "payload": { + "message": "driver.claude.message.unknown", + "raw": { + "type": "future_event", + "value": 123 + }, + "severity": "info" + }, + "visibility": "owner_debug" + } + ] +} diff --git a/tests/fixtures/providers/claude-agent-sdk/cases/unknown-message-ignored.json b/tests/fixtures/providers/claude-agent-sdk/cases/unknown-message-ignored.json deleted file mode 100644 index e68643c..0000000 --- a/tests/fixtures/providers/claude-agent-sdk/cases/unknown-message-ignored.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "runId": "run-1", - "messages": [ - { - "type": "future_event", - "value": 123 - } - ], - "expectedEvents": [] -} diff --git a/tests/fixtures/providers/openai-app-server/cases/agent-message-completed.json b/tests/fixtures/providers/openai-app-server/cases/agent-message-completed.json index e80d0ec..f157dca 100644 --- a/tests/fixtures/providers/openai-app-server/cases/agent-message-completed.json +++ b/tests/fixtures/providers/openai-app-server/cases/agent-message-completed.json @@ -3,6 +3,7 @@ { "method": "item/completed", "params": { + "completedAtMs": 1700000003000, "item": { "id": "message-1", "text": "pong", @@ -19,16 +20,8 @@ "payload": { "messageId": "", "role": "agent" - } - }, - { - "delivery": "best_effort", - "kind": "message.delta", - "payload": { - "contentDelta": "pong", - "messageId": "", - "role": "agent" - } + }, + "sourceEventId": "openai.message.started:" }, { "delivery": "lossless", @@ -37,14 +30,16 @@ "content": "pong", "messageId": "", "role": "agent" - } + }, + "sourceEventId": "openai.item.completed:turn-1:message-1:0" }, { "kind": "message.completed", "payload": { "messageId": "", "role": "agent" - } + }, + "sourceEventId": "openai.item.completed:turn-1:message-1:1" } ] } diff --git a/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json b/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json index f6e4a64..35e9c79 100644 --- a/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json +++ b/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json @@ -4,9 +4,14 @@ "method": "item/started", "params": { "item": { + "command": "printf 'hello world'", + "commandActions": [], + "cwd": "/workspace", "id": "cmd-1", + "status": "inProgress", "type": "commandExecution" }, + "startedAtMs": 1700000003000, "threadId": "thread-1", "turnId": "turn-1" } @@ -36,7 +41,8 @@ "payload": { "messageId": "", "role": "agent" - } + }, + "sourceEventId": "openai.message.started:" }, { "kind": "item.started", @@ -45,7 +51,8 @@ "itemType": "tool_call", "parentMessageId": "", "title": "Shell" - } + }, + "sourceEventId": "openai.item.started:turn-1:cmd-1" }, { "kind": "tool.call.updated", @@ -55,13 +62,13 @@ "status": "running", "title": "Shell", "toolCallId": "cmd-1" - } + }, + "sourceEventId": "openai.tool.started:turn-1:cmd-1" }, { "delivery": "best_effort", "kind": "tool.call.updated", "payload": { - "content": "hello", "messageId": "", "rawOutput": "hello", "status": "running", @@ -72,7 +79,6 @@ "delivery": "best_effort", "kind": "tool.call.updated", "payload": { - "content": " world", "messageId": "", "rawOutput": " world", "status": "running", diff --git a/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json b/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json index fafdeea..dd1c01d 100644 --- a/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json +++ b/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json @@ -5,6 +5,7 @@ "params": { "error": { "additionalDetails": "HTTP 502 from upstream.", + "codexErrorInfo": { "responseStreamDisconnected": { "httpStatusCode": 502 } }, "message": "Response stream disconnected." }, "threadId": "thread-1", @@ -19,9 +20,11 @@ "turn": { "error": { "additionalDetails": "HTTP 502 from upstream.", + "codexErrorInfo": { "responseStreamDisconnected": { "httpStatusCode": 502 } }, "message": "Response stream disconnected." }, "id": "turn-1", + "items": [], "status": "failed" } } @@ -54,9 +57,15 @@ "payload": { "error": { "code": "openai.turn_failed", - "message": "Response stream disconnected.\nHTTP 502 from upstream." + "details": { + "additionalDetails": "HTTP 502 from upstream.", + "codexErrorInfo": "responseStreamDisconnected", + "httpStatusCode": 502 + }, + "message": "Response stream disconnected.\nHTTP 502 from upstream.", + "retryable": true }, - "recoverable": false + "recoverable": true }, "sourceEventId": "openai.turn.failed:turn-1" } diff --git a/tests/fixtures/providers/openai-app-server/cases/reasoning-empty-summary.json b/tests/fixtures/providers/openai-app-server/cases/reasoning-empty-summary.json index e9acdb9..3ee00ba 100644 --- a/tests/fixtures/providers/openai-app-server/cases/reasoning-empty-summary.json +++ b/tests/fixtures/providers/openai-app-server/cases/reasoning-empty-summary.json @@ -3,6 +3,7 @@ { "method": "item/completed", "params": { + "completedAtMs": 1700000003000, "item": { "id": "reasoning-empty", "summary": [], diff --git a/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json b/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json index 98c99f7..c7e2b13 100644 --- a/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json +++ b/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json @@ -8,7 +8,7 @@ "method": "turn/started", "params": { "threadId": "thread-1", - "turn": { "id": "turn-root", "status": "inProgress" } + "turn": { "id": "turn-root", "items": [], "status": "inProgress" } } }, { @@ -26,6 +26,7 @@ "tool": "spawnAgent", "type": "collabAgentToolCall" }, + "startedAtMs": 1700000003000, "threadId": "thread-1", "turnId": "turn-root" } @@ -34,12 +35,13 @@ "method": "turn/started", "params": { "threadId": "thread-child-a", - "turn": { "id": "turn-child-a", "status": "inProgress" } + "turn": { "id": "turn-child-a", "items": [], "status": "inProgress" } } }, { "method": "item/completed", "params": { + "completedAtMs": 1700000003000, "item": { "id": "message-child-a", "text": "CHILD_A", "type": "agentMessage" }, "threadId": "thread-child-a", "turnId": "turn-child-a" @@ -61,12 +63,13 @@ "method": "turn/started", "params": { "threadId": "thread-child-b", - "turn": { "id": "turn-child-b", "status": "inProgress" } + "turn": { "id": "turn-child-b", "items": [], "status": "inProgress" } } }, { "method": "item/completed", "params": { + "completedAtMs": 1700000003000, "item": { "id": "message-child-b", "text": "CHILD_B", "type": "agentMessage" }, "threadId": "thread-child-b", "turnId": "turn-child-b" @@ -87,6 +90,7 @@ { "method": "item/completed", "params": { + "completedAtMs": 1700000003000, "item": { "agentsStates": { "thread-child-a": { "message": "CHILD_A", "status": "completed" }, @@ -109,6 +113,7 @@ { "method": "item/completed", "params": { + "completedAtMs": 1700000003000, "item": { "id": "message-parent", "text": "PARENT_OK: CHILD_A + CHILD_B", @@ -154,6 +159,7 @@ { "method": "item/completed", "params": { + "completedAtMs": 1700000003000, "item": { "id": "message-child-late", "text": "LATE_CHILD", "type": "agentMessage" }, "threadId": "thread-child-b", "turnId": "turn-child-b" @@ -174,7 +180,8 @@ }, { "kind": "message.started", - "payload": { "messageId": "", "role": "agent" } + "payload": { "messageId": "", "role": "agent" }, + "sourceEventId": "openai.message.started:" }, { "kind": "item.started", @@ -182,8 +189,9 @@ "itemId": "agent-call-1", "itemType": "tool_call", "parentMessageId": "", - "title": "spawnAgent" - } + "title": "Spawn agent" + }, + "sourceEventId": "openai.item.started:turn-root:agent-call-1" }, { "kind": "tool.call.updated", @@ -191,19 +199,31 @@ "kind": "tool", "parentMessageId": "", "status": "running", - "title": "spawnAgent", + "title": "Spawn agent", "toolCallId": "agent-call-1" - } + }, + "sourceEventId": "openai.tool.started:turn-root:agent-call-1" }, { "kind": "tool.call.updated", "payload": { - "content": "{\"thread-child-a\":{\"message\":\"CHILD_A\",\"status\":\"completed\"},\"thread-child-b\":{\"message\":\"CHILD_B\",\"status\":\"completed\"}}", - "messageId": "", - "rawOutput": "{\"thread-child-a\":{\"message\":\"CHILD_A\",\"status\":\"completed\"},\"thread-child-b\":{\"message\":\"CHILD_B\",\"status\":\"completed\"}}", "status": "completed", + "structuredOutput": { + "agentsStates": { + "thread-child-a": { "message": "CHILD_A", "status": "completed" }, + "thread-child-b": { "message": "CHILD_B", "status": "completed" } + }, + "model": "gpt-5.5", + "prompt": "Run A and B in parallel", + "reasoningEffort": "high", + "receiverThreadIds": ["thread-child-a", "thread-child-b"], + "senderThreadId": "thread-1", + "status": "completed", + "tool": "spawnAgent" + }, "toolCallId": "agent-call-1" - } + }, + "sourceEventId": "openai.item.completed:turn-root:agent-call-1:0" }, { "kind": "item.completed", @@ -211,16 +231,8 @@ "itemId": "agent-call-1", "itemType": "tool_call", "status": "completed" - } - }, - { - "delivery": "best_effort", - "kind": "message.delta", - "payload": { - "contentDelta": "PARENT_OK: CHILD_A + CHILD_B", - "messageId": "", - "role": "agent" - } + }, + "sourceEventId": "openai.item.completed:turn-root:agent-call-1:1" }, { "delivery": "lossless", @@ -229,11 +241,13 @@ "content": "PARENT_OK: CHILD_A + CHILD_B", "messageId": "", "role": "agent" - } + }, + "sourceEventId": "openai.item.completed:turn-root:message-parent:0" }, { "kind": "message.completed", - "payload": { "messageId": "", "role": "agent" } + "payload": { "messageId": "", "role": "agent" }, + "sourceEventId": "openai.item.completed:turn-root:message-parent:1" }, { "kind": "runtime.resume.updated", @@ -249,7 +263,6 @@ }, "payload": { "finalMessageId": "", - "finalMessageText": "PARENT_OK: CHILD_A + CHILD_B", "stopReason": "end_turn" }, "runId": "", diff --git a/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json b/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json index af2e0d8..f7f86c8 100644 --- a/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json +++ b/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json @@ -40,16 +40,8 @@ "payload": { "messageId": "", "role": "agent" - } - }, - { - "delivery": "best_effort", - "kind": "message.delta", - "payload": { - "contentDelta": "pong", - "messageId": "", - "role": "agent" - } + }, + "sourceEventId": "openai.message.started:" }, { "delivery": "lossless", @@ -58,14 +50,16 @@ "content": "pong", "messageId": "", "role": "agent" - } + }, + "sourceEventId": "openai.item.completed:turn-1:message-1:0" }, { "kind": "message.completed", "payload": { "messageId": "", "role": "agent" - } + }, + "sourceEventId": "openai.item.completed:turn-1:message-1:1" }, { "kind": "runtime.resume.updated", @@ -84,7 +78,6 @@ }, "payload": { "finalMessageId": "", - "finalMessageText": "pong", "stopReason": "end_turn" }, "sourceEventId": "openai.turn.completed:turn-1" diff --git a/tests/fixtures/providers/openai-app-server/cases/turn-plan-updated.json b/tests/fixtures/providers/openai-app-server/cases/turn-plan-updated.json index 2a9c578..1bbc112 100644 --- a/tests/fixtures/providers/openai-app-server/cases/turn-plan-updated.json +++ b/tests/fixtures/providers/openai-app-server/cases/turn-plan-updated.json @@ -36,7 +36,8 @@ } ], "source": "driver" - } + }, + "sourceEventId": "openai.turn.plan:turn-1:0" } ] } diff --git a/tests/fixtures/public-api-spaces-source.ts b/tests/fixtures/public-api-spaces-source.ts new file mode 100644 index 0000000..404a426 --- /dev/null +++ b/tests/fixtures/public-api-spaces-source.ts @@ -0,0 +1,16 @@ +export class AuditBoth {} +export interface AuditType { + readonly value: string; +} +export const AuditValue = 1; + +export function AuditMerged(): void {} +export declare namespace AuditMerged { + type Member = string; +} + +export declare namespace AuditDeclaredNamespace { + type Member = string; +} + +export default class AuditDefault {} diff --git a/tests/fixtures/public-api-type-namespace.ts b/tests/fixtures/public-api-type-namespace.ts new file mode 100644 index 0000000..525fbd2 --- /dev/null +++ b/tests/fixtures/public-api-type-namespace.ts @@ -0,0 +1,10 @@ +declare namespace Named { + type Member = string; +} + +export type { Named }; +export * from "./public-api-spaces-source.ts"; +export { default, default as AuditDefault } from "./public-api-spaces-source.ts"; +export * as AuditNamespace from "./public-api-spaces-source.ts"; +export type * as AuditTypeNamespace from "./public-api-spaces-source.ts"; +export type * from "./public-api-type-star-source.ts"; diff --git a/tests/fixtures/public-api-type-star-source.ts b/tests/fixtures/public-api-type-star-source.ts new file mode 100644 index 0000000..e959eea --- /dev/null +++ b/tests/fixtures/public-api-type-star-source.ts @@ -0,0 +1,5 @@ +export class TypeStarBoth {} +export interface TypeStarOnly { + readonly value: string; +} +export const TypeStarValue = 1; diff --git a/tests/openai-app-server-client.test.ts b/tests/openai-app-server-client.test.ts index 1951d51..f31e74f 100644 --- a/tests/openai-app-server-client.test.ts +++ b/tests/openai-app-server-client.test.ts @@ -14,12 +14,19 @@ import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { PermissionEventDeliveryError } from "../src/core/driver-permission-broker"; import * as childProcessHelpers from "../src/runtimes/child-process"; import { OpenAiAppServerClient, limitNdjsonLines } from "../src/runtimes/openai/app-server-client"; -import type { ServerNotificationMethod } from "../src/runtimes/openai/generated/app-server-protocol"; +import type { ServerNotificationMethod } from "../src/runtimes/openai/app-server-protocol"; import { settlePromiseWithTimeout } from "../src/utils/async"; import { driverBootPayload } from "./driver-boot-payload-fixture"; const originalExecutable = process.env["MOSOO_OPENAI_RUNTIME_EXECUTABLE"]; const temporaryDirectories: string[] = []; +const initializeResult = { + codexHome: "/tmp/openai-home", + platformFamily: "unix", + platformOs: "linux", + userAgent: "test-app-server/0.150.1", +} as const; +const initializeResultJson = JSON.stringify(initializeResult); async function createClientHarness( script: (directory: string) => string, @@ -57,21 +64,27 @@ async function createClientHarness( sink: async () => {}, }); const context = createAgentDriverContext({ - eventSink: { pushEvents: async () => ({ accepted: [] }) }, + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, logger, payload, permission: { request: requestPermission }, }); const protocolErrors: Error[] = []; + const protocolError = Promise.withResolvers(); const client = new OpenAiAppServerClient(payload, { ...context, handleNotification, handleProtocolError: async (error) => { protocolErrors.push(error); + protocolError.resolve(error); }, + mapToolCallId: (toolCallId) => toolCallId, }); - return { client, directory, logger, protocolErrors }; + return { client, directory, logger, protocolError: protocolError.promise, protocolErrors }; } afterEach(async () => { @@ -96,11 +109,17 @@ let buffer = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", async (chunk) => { buffer += chunk; - const newline = buffer.indexOf("\\n"); - if (newline < 0) return; - const request = JSON.parse(buffer.slice(0, newline)); - await Bun.write(${JSON.stringify(join(directory, "initialize.json"))}, JSON.stringify(request)); - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const request = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (request.method === "initialize") { + await Bun.write(${JSON.stringify(join(directory, "initialize.json"))}, JSON.stringify(request)); + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); + } else if (request.method === "initialized") { + await Bun.write(${JSON.stringify(join(directory, "initialized.json"))}, JSON.stringify(request)); + } + } }); setInterval(() => {}, 1000); `, @@ -116,6 +135,63 @@ setInterval(() => {}, 1000); experimentalApi: true, requestAttestation: false, }); + for ( + let attempt = 0; + attempt < 50 && !(await Bun.file(join(harness.directory, "initialized.json")).exists()); + attempt += 1 + ) { + await Bun.sleep(5); + } + expect( + JSON.parse(await Bun.file(join(harness.directory, "initialized.json")).text()), + ).toEqual({ method: "initialized" }); + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + + test("rejects a malformed approval before requesting host permission", async () => { + let permissionRequests = 0; + const harness = await createClientHarness( + () => ` +let buffer = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { + buffer += chunk; + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const message = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (message.method === "initialize") { + process.stdout.write(JSON.stringify({ id: message.id, result: ${initializeResultJson} }) + "\\n"); + } else if (message.method === "initialized") { + process.stdout.write(JSON.stringify({ + id: 41, + method: "item/commandExecution/requestApproval", + params: {}, + }) + "\\n"); + } + } +}); +setInterval(() => {}, 1000); +`, + undefined, + async () => { + permissionRequests += 1; + return "allow_once"; + }, + ); + + try { + await harness.client.start(); + + for (let attempt = 0; attempt < 50 && harness.protocolErrors.length === 0; attempt += 1) { + await Bun.sleep(5); + } + + expect(permissionRequests).toBe(0); + expect(harness.protocolErrors).toHaveLength(1); } finally { await harness.client.stop(); await harness.logger.destroy(); @@ -142,6 +218,276 @@ setInterval(() => {}, 1000); await expect(Array.fromAsync(output)).rejects.toThrow("exceeds 4 bytes"); }); + test.each([ + ["non-JSON", "not-json", "stdout is not valid JSON"], + ["non-object", "[]", "protocol message must be an object"], + ["unframed object", "{}", "requires a valid method or id"], + ] as const)("fails the protocol for %s stdout", async (_label, line, message) => { + const harness = await createClientHarness( + () => ` +process.stdin.once("data", () => { + process.stdout.write(${JSON.stringify(`${line}\n`)}); +}); +setInterval(() => {}, 1000); +`, + ); + + try { + await expect(harness.client.start()).rejects.toThrow(message); + expect(harness.protocolErrors).toHaveLength(1); + expect(harness.protocolErrors[0]?.message).toContain(message); + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + + test.each([ + [ + "both result and error", + { error: { code: -32_000, message: "failed" }, id: 1, result: initializeResult }, + ], + ["neither result nor error", { id: 1 }], + ["primitive error", { error: "failed", id: 1 }], + ["non-integer error code", { error: { code: "-32000", message: "failed" }, id: 1 }], + ["non-string error message", { error: { code: -32_000, message: 7 }, id: 1 }], + ] as const)("fails the protocol for a response with %s", async (_label, response) => { + const harness = await createClientHarness( + () => ` +process.stdin.once("data", () => { + process.stdout.write(${JSON.stringify(`${JSON.stringify(response)}\n`)}); +}); +setInterval(() => {}, 1000); +`, + ); + + try { + await expect(harness.client.start()).rejects.toThrow("response envelope is invalid"); + expect(harness.protocolErrors).toHaveLength(1); + expect(harness.protocolErrors[0]?.message).toContain("response envelope is invalid"); + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + + test("rejects a response that also claims to be a server message", async () => { + const harness = await createClientHarness( + () => ` +process.stdin.once("data", () => { + process.stdout.write(JSON.stringify({ + id: 1, + method: "warning", + params: { message: "not a response", threadId: null }, + result: ${initializeResultJson}, + }) + "\\n"); +}); +setInterval(() => {}, 1000); +`, + ); + + try { + await expect(harness.client.start()).rejects.toThrow("response envelope is invalid"); + expect(harness.protocolErrors).toHaveLength(1); + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + + test("stops after a malformed known notification", async () => { + const handled: ServerNotificationMethod[] = []; + const harness = await createClientHarness( + () => ` +let buffer = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { + buffer += chunk; + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const message = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (message.method === "initialize") { + process.stdout.write(JSON.stringify({ id: message.id, result: ${initializeResultJson} }) + "\\n"); + } else if (message.method === "initialized") { + process.stdout.write(JSON.stringify({ + method: "turn/completed", + params: { threadId: "thread-1", turn: { id: "turn-1", items: [], status: "inProgress" } }, + }) + "\\n"); + process.stdout.write(JSON.stringify({ + method: "warning", + params: { message: "must not be handled", threadId: null }, + }) + "\\n"); + } + } +}); +setInterval(() => {}, 1000); +`, + async (method) => { + handled.push(method); + }, + ); + + try { + await harness.client.start(); + for (let attempt = 0; attempt < 50 && harness.protocolErrors.length === 0; attempt += 1) { + await Bun.sleep(5); + } + + expect(harness.protocolErrors).toHaveLength(1); + expect(handled).toEqual([]); + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + + test("fails the protocol for a duplicate pending server-request id", async () => { + const permissionStarted = Promise.withResolvers(); + const permissionAborted = Promise.withResolvers(); + let permissionRequests = 0; + const harness = await createClientHarness( + () => ` +let buffer = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { + buffer += chunk; + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const message = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (message.method === "initialize") { + process.stdout.write(JSON.stringify({ id: message.id, result: ${initializeResultJson} }) + "\\n"); + } else if (message.method === "initialized") { + const request = { + id: 41, + method: "item/commandExecution/requestApproval", + params: { environmentId: null, itemId: "item-1", startedAtMs: 1, threadId: "thread-1", turnId: "turn-1" }, + }; + process.stdout.write(JSON.stringify(request) + "\\n"); + process.stdout.write(JSON.stringify(request) + "\\n"); + } + } +}); +setInterval(() => {}, 1000); +`, + undefined, + async (_input, signal) => { + permissionRequests += 1; + permissionStarted.resolve(); + await new Promise((resolve) => { + if (signal?.aborted) { + resolve(); + return; + } + signal?.addEventListener("abort", () => resolve(), { once: true }); + }); + permissionAborted.resolve(); + return "reject_once"; + }, + ); + + try { + await harness.client.start(); + await permissionStarted.promise; + for (let attempt = 0; attempt < 50 && harness.protocolErrors.length === 0; attempt += 1) { + await Bun.sleep(5); + } + + expect(harness.protocolErrors).toHaveLength(1); + expect(harness.protocolErrors[0]?.message).toContain("already pending"); + expect(permissionRequests).toBe(1); + await permissionAborted.promise; + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + + test("keeps a valid JSON-RPC error scoped to its request", async () => { + const harness = await createClientHarness( + () => ` +process.stdin.once("data", () => { + process.stdout.write(JSON.stringify({ + error: { code: -32600, data: { reason: "test" }, message: "initialize denied" }, + id: 1, + }) + "\\n"); +}); +setInterval(() => {}, 1000); +`, + ); + + try { + await expect(harness.client.start()).rejects.toThrow("initialize denied"); + expect(harness.protocolErrors).toEqual([]); + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + + test("fails the protocol when initialize returns an invalid result", async () => { + const harness = await createClientHarness( + () => ` +process.stdin.once("data", () => { + process.stdout.write(JSON.stringify({ id: 1, result: {} }) + "\\n"); +}); +setInterval(() => {}, 1000); +`, + ); + + try { + await expect(harness.client.start()).rejects.toThrow("codexHome"); + expect(harness.protocolErrors).toHaveLength(1); + await expect( + harness.client.request("initialize", { + capabilities: { experimentalApi: true, requestAttestation: false }, + clientInfo: { name: "test", title: null, version: "1" }, + }), + ).rejects.toThrow("stopping"); + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + + test("fails the protocol when turn/start returns contradictory status and error", async () => { + const harness = await createClientHarness( + () => ` +let buffer = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { + buffer += chunk; + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const request = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (request.method === "initialize") { + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); + } else if (request.method === "turn/start") { + process.stdout.write(JSON.stringify({ + id: request.id, + result: { turn: { error: null, id: "turn-1", items: [], status: "failed" } }, + }) + "\\n"); + } + } +}); +setInterval(() => {}, 1000); +`, + ); + + try { + await harness.client.start(); + await expect( + harness.client.request("turn/start", { input: [], threadId: "thread-1" }), + ).rejects.toThrow("turn.error must be present exactly when the turn failed"); + expect(harness.protocolErrors).toHaveLength(1); + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + test("rejects a child process spawn failure without an unhandled error", async () => { const harness = await createClientHarness(() => ""); process.env["MOSOO_OPENAI_RUNTIME_EXECUTABLE"] = join(harness.directory, "missing"); @@ -254,7 +600,9 @@ process.stdin.on("data", (chunk) => { const request = JSON.parse(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); if (request.id !== undefined) { - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write( + JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n", + ); } } }); @@ -295,7 +643,9 @@ process.stdin.on("data", (chunk) => { buffer = buffer.slice(newline + 1); requests += 1; if (requests === 1) { - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write( + JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n", + ); } } }); @@ -307,7 +657,7 @@ setInterval(() => {}, 1000); await harness.client.start(); const request = harness.client.request("initialize", { capabilities: { experimentalApi: true, requestAttestation: false }, - clientInfo: { name: "test", version: "1" }, + clientInfo: { name: "test", title: null, version: "1" }, }); const stop = harness.client.stop(); @@ -331,7 +681,7 @@ process.stdin.on("data", (chunk) => { const newline = buffer.indexOf("\\n"); if (newline < 0) return; const request = JSON.parse(buffer.slice(0, newline)); - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); setTimeout(() => process.exit(17), 25); }); `, @@ -378,12 +728,12 @@ process.stdin.on("data", (chunk) => { const message = JSON.parse(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); if (message.method === "initialize") { - process.stdout.write(JSON.stringify({ id: message.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: message.id, result: ${initializeResultJson} }) + "\\n"); } else if (message.method === "initialized") { process.stdout.write(JSON.stringify({ id: 41, method: "item/commandExecution/requestApproval", - params: { itemId: "item-1", threadId: "thread-1", turnId: "turn-1" }, + params: { environmentId: null, itemId: "item-1", startedAtMs: 1, threadId: "thread-1", turnId: "turn-1" }, }) + "\\n"); process.stdout.write(JSON.stringify({ method: "serverRequest/resolved", @@ -391,7 +741,7 @@ process.stdin.on("data", (chunk) => { }) + "\\n"); process.stdout.write(JSON.stringify({ method: "turn/completed", - params: { threadId: "thread-1", turn: { id: "turn-1", status: "completed" } }, + params: { threadId: "thread-1", turn: { id: "turn-1", items: [], status: "completed" } }, }) + "\\n"); } } @@ -458,12 +808,12 @@ process.stdin.on("data", (chunk) => { const message = JSON.parse(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); if (message.method === "initialize") { - process.stdout.write(JSON.stringify({ id: message.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: message.id, result: ${initializeResultJson} }) + "\\n"); } else if (message.method === "initialized") { process.stdout.write(JSON.stringify({ id: 41, method: "item/commandExecution/requestApproval", - params: { itemId: "item-1", threadId: "thread-1", turnId: "turn-1" }, + params: { environmentId: null, itemId: "item-1", startedAtMs: 1, threadId: "thread-1", turnId: "turn-1" }, }) + "\\n"); process.stdout.write(JSON.stringify({ method: "serverRequest/resolved", @@ -471,7 +821,7 @@ process.stdin.on("data", (chunk) => { }) + "\\n"); process.stdout.write(JSON.stringify({ method: "turn/completed", - params: { threadId: "thread-1", turn: { id: "turn-1", status: "completed" } }, + params: { threadId: "thread-1", turn: { id: "turn-1", items: [], status: "completed" } }, }) + "\\n"); } } @@ -545,12 +895,12 @@ process.stdin.on("data", (chunk) => { const message = JSON.parse(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); if (message.method === "initialize") { - process.stdout.write(JSON.stringify({ id: message.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: message.id, result: ${initializeResultJson} }) + "\\n"); } else if (message.method === "initialized") { process.stdout.write(JSON.stringify({ id: 41, method: "item/commandExecution/requestApproval", - params: { itemId: "item-1", threadId: "thread-1", turnId: "turn-1" }, + params: { environmentId: null, itemId: "item-1", startedAtMs: 1, threadId: "thread-1", turnId: "turn-1" }, }) + "\\n"); setTimeout(() => process.exit(17), 10); } @@ -575,23 +925,9 @@ setInterval(() => {}, 1000); try { await harness.client.start(); - const settled = await settlePromiseWithTimeout( - (async () => { - while (harness.protocolErrors.length === 0) { - await Bun.sleep(5); - } - })(), - { label: "process close", timeoutMs: 250 }, - ); + const [protocolError] = await Promise.all([harness.protocolError, permissionAborted.promise]); - expect(settled.status).toBe("completed"); - await expect( - settlePromiseWithTimeout(permissionAborted.promise, { - label: "permission abort", - timeoutMs: 250, - }), - ).resolves.toMatchObject({ status: "completed" }); - expect(harness.protocolErrors[0]?.message).toContain("code 17"); + expect(protocolError.message).toContain("code 17"); } finally { await harness.client.stop(); await harness.logger.destroy(); @@ -613,12 +949,12 @@ process.stdin.on("data", async (chunk) => { const message = JSON.parse(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); if (message.method === "initialize") { - process.stdout.write(JSON.stringify({ id: message.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: message.id, result: ${initializeResultJson} }) + "\\n"); } else if (message.method === "initialized") { process.stdout.write(JSON.stringify({ id: 41, method: "item/commandExecution/requestApproval", - params: { itemId: "item-1", threadId: "thread-1", turnId: "turn-1" }, + params: { environmentId: null, itemId: "item-1", startedAtMs: 1, threadId: "thread-1", turnId: "turn-1" }, }) + "\\n"); } else if (message.id === 41) { await Bun.write(${JSON.stringify(join(directory, "late-response.json"))}, JSON.stringify(message)); @@ -666,11 +1002,11 @@ process.stdin.on("data", (chunk) => { const message = JSON.parse(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); if (message.method === "initialize") { - process.stdout.write(JSON.stringify({ id: message.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: message.id, result: ${initializeResultJson} }) + "\\n"); } else if (message.method === "initialized") { process.stdout.write(JSON.stringify({ method: "turn/completed", - params: { threadId: "thread-1", turn: { id: "turn-1", status: "completed" } }, + params: { threadId: "thread-1", turn: { id: "turn-1", items: [], status: "completed" } }, }) + "\\n", () => process.exit(17)); } } @@ -728,7 +1064,7 @@ process.stdin.on("data", (chunk) => { const message = JSON.parse(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); if (message.method === "initialize") { - process.stdout.write(JSON.stringify({ id: message.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: message.id, result: ${initializeResultJson} }) + "\\n"); } else if (message.method === "initialized") { setTimeout(() => process.exit(0), 5); } @@ -774,7 +1110,7 @@ process.stdin.on("data", (chunk) => { const harness = await createClientHarness( () => ` IFS= read -r _ -printf '%s\n' '{"id":1,"result":{}}' +printf '%s\n' '${JSON.stringify({ id: 1, result: initializeResult })}' IFS= read -r _ exec 0<&- printf '%s\n' '{"method":"warning","params":{"message":"stdin closed","threadId":null}}' @@ -790,7 +1126,7 @@ sleep 10 await closedInput.promise; const request = harness.client.request("initialize", { capabilities: { experimentalApi: true, requestAttestation: false }, - clientInfo: { name: "x".repeat(1024 * 1024), version: "1" }, + clientInfo: { name: "x".repeat(1024 * 1024), title: null, version: "1" }, }); const settled = await settlePromiseWithTimeout(request, { label: "stdin EPIPE", @@ -824,7 +1160,9 @@ process.stdin.on("data", (chunk) => { buffer = buffer.slice(newline + 1); requests += 1; if (requests === 1) { - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write( + JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n", + ); } else { ${terminate} } @@ -838,7 +1176,7 @@ setInterval(() => {}, 1000); await harness.client.start(); const request = harness.client.request("initialize", { capabilities: { experimentalApi: true, requestAttestation: false }, - clientInfo: { name: "crash-test", version: "1" }, + clientInfo: { name: "crash-test", title: null, version: "1" }, }); await expect(request).rejects.toThrow(`app-server exited with ${expectedExit}`); @@ -872,11 +1210,15 @@ process.stdin.on("data", (chunk) => { if (request.id === undefined) continue; requests += 1; if (requests === 1) { - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write( + JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n", + ); process.stdout.write(JSON.stringify({ method: "warning", params: { message: "first", threadId: null } }) + "\\n"); } else { process.stdout.write(JSON.stringify({ method: "warning", params: { message: "second", threadId: null } }) + "\\n"); - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write( + JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n", + ); } } }); @@ -903,7 +1245,7 @@ setInterval(() => {}, 1000); }); await harness.client.request("initialize", { capabilities: { experimentalApi: true, requestAttestation: false }, - clientInfo: { name: "drain-test", version: "1" }, + clientInfo: { name: "drain-test", title: null, version: "1" }, }); expect(drained).toBe(false); @@ -931,7 +1273,7 @@ process.stdin.on("data", (chunk) => { const newline = buffer.indexOf("\\n"); if (newline < 0) return; const request = JSON.parse(buffer.slice(0, newline)); - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); }); process.on("SIGTERM", async () => { await Bun.write(${JSON.stringify(join(directory, "stopped"))}, "stopped"); @@ -986,7 +1328,7 @@ process.stdin.on("data", (chunk) => { const newline = buffer.indexOf("\\n"); if (newline < 0) return; const request = JSON.parse(buffer.slice(0, newline)); - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n", () => process.exit(0)); + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n", () => process.exit(0)); }); `; }); @@ -1045,7 +1387,7 @@ process.stdin.on("data", (chunk) => { const newline = buffer.indexOf("\\n"); if (newline < 0) return; const request = JSON.parse(buffer.slice(0, newline)); - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); }); process.on("SIGTERM", () => process.exit(0)); setInterval(() => {}, 1000); @@ -1110,7 +1452,7 @@ process.stdin.on("data", (chunk) => { const newline = buffer.indexOf("\\n"); if (newline < 0) return; const request = JSON.parse(buffer.slice(0, newline)); - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); }); process.on("SIGTERM", () => process.exit(0)); setInterval(() => {}, 1000); @@ -1176,7 +1518,7 @@ process.stdin.on("data", (chunk) => { const newline = buffer.indexOf("\\n"); if (newline < 0) return; const request = JSON.parse(buffer.slice(0, newline)); - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); }); process.on("SIGTERM", () => process.exit(0)); setInterval(() => {}, 1000); @@ -1231,7 +1573,9 @@ process.stdin.on("data", (chunk) => { while ((newline = buffer.indexOf("\\n")) >= 0) { const request = JSON.parse(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write( + JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n", + ); } }); process.on("SIGTERM", () => setTimeout(() => process.exit(0), 200)); @@ -1244,7 +1588,7 @@ setInterval(() => {}, 1000); const stop = harness.client.stop(); const request = harness.client.request("initialize", { capabilities: { experimentalApi: true, requestAttestation: false }, - clientInfo: { name: "shutdown-race", version: "1" }, + clientInfo: { name: "shutdown-race", title: null, version: "1" }, }); const outcome = await settlePromiseWithTimeout(request, { label: "request after client shutdown", @@ -1277,7 +1621,7 @@ process.stdin.on("data", (chunk) => { const request = JSON.parse(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); if (request.id === undefined) return; - process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n"); + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); for (let index = 0; index < 2048; index += 1) { process.stdout.write(JSON.stringify({ method: "item/agentMessage/delta", diff --git a/tests/openai-app-server-event-bridge-fixture.ts b/tests/openai-app-server-event-bridge-fixture.ts new file mode 100644 index 0000000..cc010b0 --- /dev/null +++ b/tests/openai-app-server-event-bridge-fixture.ts @@ -0,0 +1,131 @@ +import { expect } from "bun:test"; + +import type { AgentDriverContext } from "../src/core/agent-driver-backend"; +import { createAgentDriverContext } from "../src/core/agent-driver-backend"; +import { createBufferedSinkLogger } from "../src/observability"; +import type { DriverEventInput } from "../src/protocol/events"; +import { isDriverId } from "../src/protocol/id"; +import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; +import { DRIVER_TEST_IDS, driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; + +interface EventBatch { + events: DriverEventInput[]; + reason: string; +} + +export function readEventPayloadString(event: DriverEventInput, field: string): string | null { + const payload = event.payload; + + if (typeof payload !== "object" || payload === null || Array.isArray(payload)) { + return null; + } + + const value = (payload as Record)[field]; + return typeof value === "string" ? value : null; +} + +export function readAssistantMessageId(events: readonly DriverEventInput[]): string { + for (const event of events) { + const messageId = + readEventPayloadString(event, "messageId") ?? + readEventPayloadString(event, "parentMessageId"); + + if (messageId !== null) { + expect(isDriverId(messageId)).toBe(true); + return messageId; + } + } + + throw new Error("Expected a platform assistant message ID."); +} + +export function createOpenAiBridgeHarness( + options: { + failNativeResumePublish?: boolean; + failReasonOnce?: string; + failTerminalOnce?: boolean; + holdFailedTerminalOnce?: boolean; + holdReason?: string; + } = {}, +) { + const batches: EventBatch[] = []; + const attempts: EventBatch[] = []; + const terminalAttempts: Array<{ + closures: readonly DriverEventInput[]; + terminal: DriverEventInput; + }> = []; + let failedReason = false; + let failedTerminal = false; + const heldPush = Promise.withResolvers(); + const releasePush = Promise.withResolvers(); + const terminalHeld = Promise.withResolvers(); + const releaseTerminal = Promise.withResolvers(); + const logger = createBufferedSinkLogger({ + level: "debug", + service: "openai-app-server-event-bridge-test", + sink: async () => {}, + }); + const context: AgentDriverContext = createAgentDriverContext({ + eventSink: { + currentRunId: () => DRIVER_TEST_IDS.runId, + pushEvents: async () => ({ accepted: [] }), + }, + logger, + payload: bootPayload, + permission: { + request: async () => "allow_once", + }, + }); + const push = async (_context: AgentDriverContext, reason: string, events: DriverEventInput[]) => { + attempts.push({ events, reason }); + if (!failedReason && reason === options.failReasonOnce) { + failedReason = true; + throw new Error("event sink rejected the first attempt"); + } + batches.push({ events, reason }); + if (reason === options.holdReason) { + heldPush.resolve(); + await releasePush.promise; + } + if ( + options.failNativeResumePublish === true && + reason === "driver.openai.native_resume_ref.updated" + ) { + throw new Error("event sink unavailable"); + } + }; + const bridge = new OpenAiAppServerEventBridge({ + push, + pushSession: push, + pushTerminal: async (pushContext, reason, closures, terminal) => { + terminalAttempts.push({ closures, terminal }); + if (!failedTerminal && options.failTerminalOnce === true) { + failedTerminal = true; + if (options.holdFailedTerminalOnce === true) { + terminalHeld.resolve(); + await releaseTerminal.promise; + } + throw new Error("terminal sink rejected the first attempt"); + } + for (const closure of closures) { + await push(pushContext, `${reason}.items`, [closure]); + } + await push(pushContext, reason, [terminal]); + }, + requireThreadId: () => "thread-1", + }); + + return { + attempts, + batches, + bridge, + context, + events: () => batches.flatMap((batch) => batch.events), + heldPush: heldPush.promise, + logger, + releasePush: releasePush.resolve, + releaseTerminal: releaseTerminal.resolve, + terminalHeld: terminalHeld.promise, + terminalAttempts, + }; +} diff --git a/tests/openai-app-server-event-bridge-items.test.ts b/tests/openai-app-server-event-bridge-items.test.ts index b078d74..79b0d35 100644 --- a/tests/openai-app-server-event-bridge-items.test.ts +++ b/tests/openai-app-server-event-bridge-items.test.ts @@ -1,78 +1,46 @@ import { describe, expect, test } from "bun:test"; -import { createBufferedSinkLogger } from "../src/observability"; -import type { DriverEventInput } from "../src/protocol/events"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; +import { OpenAiPublicIdState } from "../src/runtimes/openai/app-server-event-state"; +import { parseServerNotification } from "../src/runtimes/openai/app-server-protocol-server"; import { OpenAiTurnTracker } from "../src/runtimes/openai/app-server-turn-tracker"; import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; -import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; - -interface EventBatch { - events: DriverEventInput[]; - reason: string; -} - -function readEventPayloadString(event: DriverEventInput, field: string): string | null { - const payload = event.payload; - - if (typeof payload !== "object" || payload === null || Array.isArray(payload)) { - return null; +import { + createOpenAiBridgeHarness as createHarness, + readEventPayloadString, +} from "./openai-app-server-event-bridge-fixture"; + +async function handleOfficialNotification( + bridge: OpenAiAppServerEventBridge, + context: AgentDriverContext, + method: string, + params: Record, +): Promise { + const notification = parseServerNotification({ method, params }); + + expect(notification).not.toBeNull(); + if (notification === null) { + throw new Error(`Expected ${method} to be an official OpenAI notification.`); } - const value = (payload as Record)[field]; - return typeof value === "string" ? value : null; + await bridge.handleNotification(context, notification.method, notification.params); } -function createHarness(options: { failNativeResumePublish?: boolean; holdReason?: string } = {}) { - const batches: EventBatch[] = []; - const heldPush = Promise.withResolvers(); - const releasePush = Promise.withResolvers(); - const logger = createBufferedSinkLogger({ - level: "debug", - service: "openai-app-server-event-bridge-test", - sink: async () => {}, - }); - const context: AgentDriverContext = createAgentDriverContext({ - eventSink: { - pushEvents: async () => ({ accepted: [] }), - }, - logger, - payload: bootPayload, - permission: { - request: async () => "allow_once", - }, - }); - const bridge = new OpenAiAppServerEventBridge({ - push: async (_context, reason, events) => { - batches.push({ events, reason }); - if (reason === options.holdReason) { - heldPush.resolve(); - await releasePush.promise; - } - if ( - options.failNativeResumePublish === true && - reason === "driver.openai.native_resume_ref.updated" - ) { - throw new Error("event sink unavailable"); - } - }, - requireThreadId: () => "thread-1", +describe("OpenAi app-server event bridge", () => { + test("keeps bounded provider identities stable and namespace distinct", () => { + const ids = new OpenAiPublicIdState(); + const nativeId = "native".repeat(100); + const publicItemId = ids.publicId(nativeId, "item"); + const publicTurnId = ids.publicId(nativeId, "turn"); + + expect(ids.publicId(nativeId, "item")).toBe(publicItemId); + expect(ids.publicId(nativeId, "turn")).toBe(publicTurnId); + expect(publicItemId).not.toBe(publicTurnId); + expect(publicItemId.length).toBeLessThan(nativeId.length); + expect(publicTurnId.length).toBeLessThan(nativeId.length); }); - return { - batches, - bridge, - context, - events: () => batches.flatMap((batch) => batch.events), - heldPush: heldPush.promise, - logger, - releasePush: releasePush.resolve, - }; -} - -describe("OpenAi app-server event bridge", () => { test("bounds terminal turn deduplication and clears it on shutdown", () => { const tracker = new OpenAiTurnTracker(); @@ -112,6 +80,19 @@ describe("OpenAi app-server event bridge", () => { expect(tracker.hasTerminal("turn-1")).toBe(true); }); + test("rejects settling turns with bounded retained identities", async () => { + const tracker = new OpenAiTurnTracker(); + const turnId = "turn".repeat(150_000); + const tracked = tracker.track(turnId, DRIVER_TEST_IDS.runId); + + expect(tracker.beginSettlement(turnId)).toBe(true); + tracker.rejectActiveTurns(new Error("driver stopped")); + + await expect(tracked).rejects.toThrow("driver stopped"); + expect(tracker.activeTurnIds()).toEqual([]); + expect(tracker.hasTerminal(turnId)).toBe(true); + }); + test("shares duplicate turn tracking without orphaning the first waiter", async () => { const tracker = new OpenAiTurnTracker(); const first = tracker.track("turn-1", DRIVER_TEST_IDS.runId); @@ -131,6 +112,7 @@ describe("OpenAi app-server event bridge", () => { await bridge.handleNotification(context, "item/started", { item: { id: "tool-1", + status: "inProgress", type: "commandExecution", }, threadId: "thread-1", @@ -144,9 +126,9 @@ describe("OpenAi app-server event bridge", () => { { kind: "item.started" }, { kind: "tool.call.updated", payload: { status: "running", toolCallId: "tool-1" } }, { kind: "run.cancel.requested", runId: DRIVER_TEST_IDS.runId }, - { kind: "message.completed" }, - { kind: "tool.call.updated", payload: { status: "failed", toolCallId: "tool-1" } }, - { kind: "item.completed", payload: { itemId: "tool-1", status: "failed" } }, + { kind: "message.cancelled" }, + { kind: "tool.call.updated", payload: { status: "cancelled", toolCallId: "tool-1" } }, + { kind: "item.completed", payload: { itemId: "tool-1", status: "cancelled" } }, { kind: "run.cancelled", runId: DRIVER_TEST_IDS.runId }, ]); const terminalEventCount = events().length; @@ -164,12 +146,60 @@ describe("OpenAi app-server event bridge", () => { await logger.destroy(); }); + test("accepts only a completed sub-agent activity after its parent turn terminal", async () => { + const { bridge, context, events, logger } = createHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, + }); + await trackedTurn; + const terminalEventCount = events().length; + + await bridge.handleNotification(context, "item/completed", { + item: { + agentPath: "/root/worker", + agentThreadId: "agent-1", + id: "activity-1", + kind: "completed", + type: "subAgentActivity", + }, + threadId: "thread-1", + turnId: "turn-1", + }); + expect(events().slice(terminalEventCount)).toEqual([ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { + active: false, + activityKind: "completed", + agentId: "agent-1", + agentPath: "/root/worker", + status: "completed", + taskId: "agent-1", + title: "Sub-agent completed", + }, + sourceEventId: "openai.item.completed:turn-1:activity-1:0", + }, + ]); + + await bridge.handleNotification(context, "item/completed", { + item: { id: "late-message", text: "late", type: "agentMessage" }, + threadId: "thread-1", + turnId: "turn-1", + }); + expect(events()).toHaveLength(terminalEventCount + 1); + await logger.destroy(); + }); + test("closes open item state when the provider interrupts a turn", async () => { const { bridge, context, events, logger } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "item/started", { - item: { id: "tool-1", type: "commandExecution" }, + item: { id: "tool-1", status: "inProgress", type: "commandExecution" }, threadId: "thread-1", turnId: "turn-1", }); @@ -180,9 +210,9 @@ describe("OpenAi app-server event bridge", () => { await expect(trackedTurn).rejects.toThrow("interrupted"); expect(events().slice(-4)).toMatchObject([ - { kind: "message.completed" }, - { kind: "tool.call.updated", payload: { status: "failed", toolCallId: "tool-1" } }, - { kind: "item.completed", payload: { itemId: "tool-1", status: "failed" } }, + { kind: "message.cancelled" }, + { kind: "tool.call.updated", payload: { status: "cancelled", toolCallId: "tool-1" } }, + { kind: "item.completed", payload: { itemId: "tool-1", status: "cancelled" } }, { kind: "run.cancelled", payload: { requestedBy: "provider", stopReason: "cancelled" }, @@ -197,14 +227,14 @@ describe("OpenAi app-server event bridge", () => { const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "item/started", { - item: { id: "tool-1", type: "commandExecution" }, + item: { id: "tool-1", status: "inProgress", type: "commandExecution" }, threadId: "thread-1", turnId: "turn-1", }); await bridge.handleNotification(context, "turn/completed", { threadId: "thread-1", turn: { - error: { additionalDetails: null, message: "command failed" }, + error: { additionalDetails: null, codexErrorInfo: null, message: "command failed" }, id: "turn-1", status: "failed", }, @@ -212,7 +242,12 @@ describe("OpenAi app-server event bridge", () => { await expect(trackedTurn).rejects.toThrow("command failed"); expect(events().slice(-4)).toMatchObject([ - { kind: "message.completed" }, + { + kind: "message.failed", + payload: { + error: { code: "openai.turn_failed", message: "command failed", retryable: false }, + }, + }, { kind: "tool.call.updated", payload: { status: "failed", toolCallId: "tool-1" } }, { kind: "item.completed", payload: { itemId: "tool-1", status: "failed" } }, { kind: "run.failed", runId: DRIVER_TEST_IDS.runId }, @@ -225,7 +260,7 @@ describe("OpenAi app-server event bridge", () => { const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "item/started", { - item: { id: "tool-1", type: "commandExecution" }, + item: { id: "tool-1", status: "inProgress", type: "commandExecution" }, threadId: "thread-1", turnId: "turn-1", }); @@ -238,35 +273,47 @@ describe("OpenAi app-server event bridge", () => { const terminalIndex = events().findIndex((event) => event.kind === "run.completed"); expect(events().slice(terminalIndex - 3, terminalIndex + 1)).toMatchObject([ { kind: "message.completed" }, - { kind: "tool.call.updated", payload: { status: "failed", toolCallId: "tool-1" } }, - { kind: "item.completed", payload: { itemId: "tool-1", status: "failed" } }, + { kind: "tool.call.updated", payload: { status: "completed", toolCallId: "tool-1" } }, + { kind: "item.completed", payload: { itemId: "tool-1", status: "completed" } }, { kind: "run.completed", runId: DRIVER_TEST_IDS.runId }, ]); await logger.destroy(); }); - test("keeps cancellation terminal while turn completion is awaiting item delivery", async () => { + test("drains pending item delivery before cancellation and leaves the next turn clean", async () => { const harness = createHarness({ holdReason: "driver.openai.item.completed" }); const trackedTurn = harness.bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await harness.bridge.handleNotification(harness.context, "item/started", { - item: { id: "tool-1", type: "commandExecution" }, + item: { id: "tool-1", status: "inProgress", type: "commandExecution" }, threadId: "thread-1", turnId: "turn-1", }); - const completion = harness.bridge.handleNotification(harness.context, "turn/completed", { - threadId: "thread-1", - turn: { - id: "turn-1", - items: [{ aggregatedOutput: "done", id: "tool-1", type: "commandExecution" }], - itemsView: "full", + const completion = harness.bridge.handleNotification(harness.context, "item/completed", { + item: { + aggregatedOutput: "done", + id: "tool-1", status: "completed", + type: "commandExecution", }, + threadId: "thread-1", + turnId: "turn-1", }); await harness.heldPush; - await harness.bridge.cancelTurn(harness.context, "turn-1", "test.cancel"); + const cancellation = harness.bridge.cancelTurn( + harness.context, + "turn-1", + "test.cancel", + async () => completion, + ); + let cancellationSettled = false; + void cancellation.finally(() => { + cancellationSettled = true; + }); + await Promise.resolve(); + expect(cancellationSettled).toBe(false); harness.releasePush(); - await completion; + await cancellation; await expect(trackedTurn).rejects.toThrow("test.cancel"); await expect(harness.bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId)).rejects.toThrow( @@ -277,6 +324,26 @@ describe("OpenAi app-server event bridge", () => { .events() .filter((event) => ["run.cancelled", "run.completed", "run.failed"].includes(event.kind)), ).toMatchObject([{ kind: "run.cancelled", runId: DRIVER_TEST_IDS.runId }]); + + const nextTurn = harness.bridge.trackTurn("turn-2", DRIVER_TEST_IDS.secondRunId); + await harness.bridge.handleNotification(harness.context, "item/started", { + item: { id: "tool-1", status: "inProgress", type: "commandExecution" }, + threadId: "thread-1", + turnId: "turn-2", + }); + await harness.bridge.handleNotification(harness.context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-2", items: [], itemsView: "notLoaded", status: "completed" }, + }); + await expect(nextTurn).resolves.toBeUndefined(); + expect( + harness + .events() + .filter( + (event) => + event.kind === "item.started" && readEventPayloadString(event, "itemId") === "tool-1", + ), + ).toHaveLength(2); await harness.logger.destroy(); }); @@ -309,6 +376,199 @@ describe("OpenAi app-server event bridge", () => { await logger.destroy(); }); + test("replays an item completion after delivery rejection before committing state", async () => { + const harness = createHarness({ failReasonOnce: "driver.openai.item.completed" }); + const notification = { + item: { + id: "message-1", + phase: "final_answer", + text: "authoritative answer", + type: "agentMessage", + }, + threadId: "thread-1", + turnId: "turn-1", + } as const; + + await expect( + harness.bridge.handleNotification(harness.context, "item/completed", notification), + ).rejects.toThrow("first attempt"); + await harness.bridge.handleNotification(harness.context, "item/completed", notification); + await harness.bridge.handleNotification(harness.context, "item/completed", notification); + + const attempts = harness.attempts.filter( + ({ reason }) => reason === "driver.openai.item.completed", + ); + expect(attempts).toHaveLength(2); + expect(attempts[0]!.events.map(({ sourceEventId }) => sourceEventId)).toEqual( + attempts[1]!.events.map(({ sourceEventId }) => sourceEventId), + ); + expect( + harness + .events() + .filter((event) => event.kind === "message.added" || event.kind === "message.completed"), + ).toHaveLength(2); + await harness.logger.destroy(); + }); + + test("replays a plan delta after delivery rejection without duplicating content", async () => { + const harness = createHarness({ failReasonOnce: "driver.openai.plan.delta" }); + const notification = { + delta: "step", + itemId: "plan-1", + threadId: "thread-1", + turnId: "turn-1", + } as const; + + await expect( + harness.bridge.handleNotification(harness.context, "item/plan/delta", notification), + ).rejects.toThrow("first attempt"); + await harness.bridge.handleNotification(harness.context, "item/plan/delta", notification); + + await harness.bridge.handleNotification(harness.context, "item/plan/delta", { + ...notification, + delta: " two", + }); + + const accepted = harness.batches.filter((batch) => batch.reason === "driver.openai.plan.delta"); + expect(accepted.at(-1)?.events[0]?.payload).toMatchObject({ + entries: [{ content: "step two", status: "in_progress" }], + }); + expect(accepted.map((batch) => batch.events[0]?.sourceEventId)).toEqual([ + "openai.plan.delta:turn-1:plan-1:0", + "openai.plan.delta:turn-1:plan-1:4", + ]); + expect( + harness.attempts + .filter((batch) => batch.reason === "driver.openai.plan.delta") + .slice(0, 2) + .map((batch) => batch.events[0]?.sourceEventId), + ).toEqual(["openai.plan.delta:turn-1:plan-1:0", "openai.plan.delta:turn-1:plan-1:0"]); + expect(JSON.stringify(accepted)).not.toContain("stepstep"); + await harness.logger.destroy(); + }); + + test("retries message start with the same identity after delivery rejection", async () => { + const harness = createHarness({ failReasonOnce: "driver.openai.message.started" }); + const notification = { + delta: "hello", + itemId: "message-1", + threadId: "thread-1", + turnId: "turn-1", + } as const; + + await expect( + harness.bridge.handleNotification(harness.context, "item/agentMessage/delta", notification), + ).rejects.toThrow("first attempt"); + await harness.bridge.handleNotification( + harness.context, + "item/agentMessage/delta", + notification, + ); + + const attempts = harness.attempts.filter( + ({ reason }) => reason === "driver.openai.message.started", + ); + expect(attempts).toHaveLength(2); + expect(attempts[0]!.events[0]?.sourceEventId).toBe(attempts[1]!.events[0]?.sourceEventId); + expect(harness.events().filter((event) => event.kind === "message.started")).toHaveLength(1); + expect( + harness + .events() + .filter((event) => event.kind === "message.delta") + .map((event) => readEventPayloadString(event, "contentDelta")), + ).toEqual(["hello"]); + await harness.logger.destroy(); + }); + + test("replays terminal closures after rejection before settling the turn", async () => { + const harness = createHarness({ failTerminalOnce: true }); + const trackedTurn = harness.bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + await harness.bridge.handleNotification(harness.context, "item/started", { + item: { id: "tool-1", status: "inProgress", type: "commandExecution" }, + threadId: "thread-1", + turnId: "turn-1", + }); + const completion = { + threadId: "thread-1", + turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, + } as const; + + await expect( + harness.bridge.handleNotification(harness.context, "turn/completed", completion), + ).rejects.toThrow("first attempt"); + await harness.bridge.handleNotification(harness.context, "turn/completed", completion); + await expect(trackedTurn).resolves.toBeUndefined(); + + expect(harness.terminalAttempts).toHaveLength(2); + expect(harness.terminalAttempts[0]!.closures).toEqual(harness.terminalAttempts[1]!.closures); + expect( + harness + .events() + .filter((event) => event.kind === "tool.call.updated") + .map((event) => readEventPayloadString(event, "status")), + ).toEqual(["running", "completed"]); + expect(harness.events().filter((event) => event.kind === "run.completed")).toHaveLength(1); + await harness.logger.destroy(); + }); + + test("replays cancellation closures after rejection before rejecting the turn", async () => { + const harness = createHarness({ failTerminalOnce: true }); + const trackedTurn = harness.bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + await harness.bridge.handleNotification(harness.context, "item/started", { + item: { id: "tool-1", status: "inProgress", type: "commandExecution" }, + threadId: "thread-1", + turnId: "turn-1", + }); + + await expect( + harness.bridge.cancelTurn(harness.context, "turn-1", "test.cancel"), + ).rejects.toThrow("first attempt"); + await harness.bridge.cancelTurn(harness.context, "turn-1", "test.cancel"); + await expect(trackedTurn).rejects.toThrow("test.cancel"); + + expect(harness.terminalAttempts).toHaveLength(2); + expect(harness.terminalAttempts[0]!.closures).toEqual(harness.terminalAttempts[1]!.closures); + expect(harness.events().filter((event) => event.kind === "run.cancelled")).toHaveLength(1); + await harness.logger.destroy(); + }); + + test("waits for a failed concurrent settlement before claiming cancellation", async () => { + const harness = createHarness({ failTerminalOnce: true, holdFailedTerminalOnce: true }); + const trackedTurn = harness.bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const completion = harness.bridge.handleNotification(harness.context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, + }); + void completion.catch(() => {}); + await harness.terminalHeld; + let drained = false; + const cancellation = harness.bridge.cancelTurn( + harness.context, + "turn-1", + "test.cancel", + async () => { + await completion.catch(() => {}); + drained = true; + }, + ); + + await Promise.resolve(); + expect(drained).toBe(false); + harness.releaseTerminal(); + await expect(completion).rejects.toThrow("first attempt"); + await cancellation; + await expect(trackedTurn).rejects.toThrow("test.cancel"); + expect(drained).toBe(true); + expect(harness.terminalAttempts.map(({ terminal }) => terminal.kind)).toEqual([ + "run.completed", + "run.cancelled", + ]); + await harness.logger.destroy(); + }); + test("orders cancellation after an in-flight item update", async () => { const { bridge, context, events, heldPush, logger, releasePush } = createHarness({ holdReason: "driver.openai.message.started", @@ -317,6 +577,7 @@ describe("OpenAi app-server event bridge", () => { const itemUpdate = bridge.handleNotification(context, "item/started", { item: { id: "tool-1", + status: "inProgress", type: "commandExecution", }, threadId: "thread-1", @@ -343,4 +604,324 @@ describe("OpenAi app-server event bridge", () => { ).toBeLessThan(events().findLastIndex((event) => event.kind === "run.cancelled")); await logger.destroy(); }); + + test("maps hooks and automatic approval reviews to their exact lifecycle events", async () => { + const { bridge, context, events, logger } = createHarness(); + + const hookRun = { + displayOrder: 0, + entries: [], + eventName: "interrupt", + executionMode: "sync", + handlerType: "command", + id: "hook-1", + scope: "turn", + sourcePath: "/tmp/hook", + startedAt: 1, + }; + const action = { command: "echo ok", cwd: "/tmp", source: "shell", type: "command" }; + + await handleOfficialNotification(bridge, context, "hook/started", { + run: { ...hookRun, status: "running" }, + threadId: "thread-1", + turnId: "turn-1", + }); + await handleOfficialNotification(bridge, context, "hook/completed", { + run: { ...hookRun, completedAt: 2, durationMs: 1, status: "completed" }, + threadId: "thread-1", + turnId: "turn-1", + }); + await handleOfficialNotification(bridge, context, "item/autoApprovalReview/started", { + action, + review: { status: "inProgress" }, + reviewId: "review-1", + startedAtMs: 1, + targetItemId: "tool-1", + threadId: "thread-1", + turnId: "turn-1", + }); + await handleOfficialNotification(bridge, context, "item/autoApprovalReview/completed", { + action, + completedAtMs: 2, + decisionSource: "agent", + review: { riskLevel: "low", status: "approved", userAuthorization: "high" }, + reviewId: "review-1", + startedAtMs: 1, + targetItemId: "tool-1", + threadId: "thread-1", + turnId: "turn-1", + }); + + expect(events().map((event) => event.kind)).toEqual([ + "hook.started", + "hook.completed", + "permission.review.started", + "permission.review.completed", + ]); + expect(events()[2]?.payload).toMatchObject({ reviewId: "review-1", targetItemId: "tool-1" }); + await logger.destroy(); + }); + + test("preserves bounded permission profiles in automatic approval reviews", async () => { + const { bridge, context, events, logger } = createHarness(); + const permissions = { + fileSystem: { read: ["/workspace"], write: ["/tmp"] }, + network: { enabled: true }, + }; + + await handleOfficialNotification(bridge, context, "item/autoApprovalReview/started", { + action: { permissions, reason: "Inspect generated files", type: "requestPermissions" }, + review: { status: "inProgress" }, + reviewId: "review-permissions", + startedAtMs: 1, + targetItemId: null, + threadId: "thread-1", + turnId: "turn-1", + }); + + expect(events()[0]?.payload).toMatchObject({ + action: { + permissions, + reason: "Inspect generated files", + type: "requestPermissions", + }, + }); + + await handleOfficialNotification(bridge, context, "item/autoApprovalReview/started", { + action: { + approvalId: "approval-1", + cwd: "/workspace", + processId: "process-1", + stdin: "secret input\n", + type: "writeStdin", + }, + review: { status: "inProgress" }, + reviewId: "review-stdin", + startedAtMs: 2, + targetItemId: "command-1", + threadId: "thread-1", + turnId: "turn-1", + }); + expect(events()[1]?.payload).toMatchObject({ + action: { + approvalId: "approval-1", + cwd: "/workspace", + processId: "process-1", + stdinUtf8Bytes: 13, + type: "writeStdin", + }, + }); + await handleOfficialNotification(bridge, context, "item/autoApprovalReview/completed", { + action: { + approvalId: "approval-1", + cwd: "/workspace", + processId: "process-1", + stdin: "secret input\n", + type: "writeStdin", + }, + completedAtMs: 3, + decisionSource: "agent", + review: { + rationale: "The terminal input contains secret input", + riskLevel: "high", + status: "denied", + userAuthorization: "high", + }, + reviewId: "review-stdin", + startedAtMs: 2, + targetItemId: "command-1", + threadId: "thread-1", + turnId: "turn-1", + }); + expect(events()[2]?.payload).toMatchObject({ + review: { riskLevel: "high", status: "denied", userAuthorization: "high" }, + }); + expect(JSON.stringify(events().slice(1))).not.toContain("secret input"); + await logger.destroy(); + }); + + test("keeps mapped MCP progress and terminal interaction visible", async () => { + const { bridge, context, events, logger } = createHarness(); + const nativeItemId = `mcp-${"x".repeat(300)}`; + const nativeCommandId = `command-${"y".repeat(300)}`; + + await handleOfficialNotification(bridge, context, "item/mcpToolCall/progress", { + itemId: "unknown-mcp", + message: "ignored", + threadId: "thread-1", + turnId: "turn-1", + }); + expect(events()).toEqual([]); + await handleOfficialNotification(bridge, context, "item/started", { + item: { + arguments: { path: "src" }, + id: nativeItemId, + server: "filesystem", + status: "inProgress", + tool: "inspect", + type: "mcpToolCall", + }, + startedAtMs: 1, + threadId: "thread-1", + turnId: "turn-1", + }); + const publicToolCallId = readEventPayloadString( + events().find((event) => event.kind === "item.started")!, + "itemId", + ); + expect(publicToolCallId).not.toBeNull(); + expect(publicToolCallId).not.toBe(nativeItemId); + + await handleOfficialNotification(bridge, context, "item/mcpToolCall/progress", { + itemId: nativeItemId, + message: "Reading project files", + threadId: "thread-1", + turnId: "turn-1", + }); + await handleOfficialNotification(bridge, context, "item/commandExecution/terminalInteraction", { + itemId: "unknown-command", + processId: "process-1", + stdin: "ignored\n", + threadId: "thread-1", + turnId: "turn-1", + }); + expect(events().some((event) => event.kind === "shell.command.updated")).toBe(false); + await handleOfficialNotification(bridge, context, "item/started", { + item: { + aggregatedOutput: null, + command: "printf done", + commandActions: [], + cwd: "/workspace", + durationMs: null, + exitCode: null, + id: nativeCommandId, + pluginId: null, + processId: null, + scriptPath: null, + source: "agent", + status: "inProgress", + type: "commandExecution", + }, + startedAtMs: 1, + threadId: "thread-1", + turnId: "turn-1", + }); + const publicCommandId = readEventPayloadString( + events() + .filter((event) => event.kind === "item.started") + .at(-1)!, + "itemId", + ); + expect(publicCommandId).not.toBe(nativeCommandId); + await handleOfficialNotification(bridge, context, "item/commandExecution/terminalInteraction", { + itemId: nativeCommandId, + processId: "process-1", + stdin: "y\n", + threadId: "thread-1", + turnId: "turn-1", + }); + + expect( + events().find( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "rawOutput") === "Reading project files", + ), + ).toMatchObject({ + delivery: "best_effort", + payload: { + status: "running", + toolCallId: publicToolCallId, + }, + }); + expect(events().at(-1)).toMatchObject({ + delivery: "best_effort", + kind: "shell.command.updated", + payload: { + itemId: publicCommandId, + processId: "process-1", + status: "running", + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(JSON.stringify(events())).not.toContain("y\\n"); + await logger.destroy(); + }); + + test("publishes model, MCP server, and user-facing warning notifications", async () => { + const { bridge, context, events, logger } = createHarness(); + + await handleOfficialNotification(bridge, context, "mcpServer/startupStatus/updated", { + error: null, + failureReason: null, + name: "filesystem", + status: "ready", + threadId: "thread-1", + }); + await handleOfficialNotification(bridge, context, "model/rerouted", { + fromModel: "gpt-a", + reason: "highRiskCyberActivity", + threadId: "thread-1", + toModel: "gpt-b", + turnId: "turn-1", + }); + await handleOfficialNotification(bridge, context, "model/verification", { + threadId: "thread-1", + turnId: "turn-1", + verifications: [], + }); + await handleOfficialNotification(bridge, context, "model/safetyBuffering/updated", { + fasterModel: null, + model: "gpt-b", + reasons: ["policy"], + showBufferingUi: true, + threadId: "thread-1", + turnId: "turn-1", + useCases: ["agent"], + }); + await handleOfficialNotification(bridge, context, "warning", { + message: "Provider warning", + threadId: "thread-1", + }); + await handleOfficialNotification(bridge, context, "guardianWarning", { + message: "Guardian warning", + threadId: "thread-1", + }); + await handleOfficialNotification(bridge, context, "windows/worldWritableWarning", { + extraCount: 2, + failedScan: true, + samplePaths: ["C:\\unsafe-one", "C:\\unsafe-two"], + }); + + expect(events().map((event) => event.kind)).toEqual([ + "mcp.server.updated", + "model.routing.updated", + "model.verification.updated", + "model.routing.updated", + "message.added", + "message.added", + "message.added", + ]); + expect( + events() + .slice(-3) + .map((event) => event.payload), + ).toMatchObject([ + { content: "Provider warning", level: "warning" }, + { content: "Guardian warning", level: "warning" }, + { + extraCount: 2, + failedScan: true, + level: "warning", + samplePaths: ["C:\\unsafe-one", "C:\\unsafe-two"], + subtype: "windows_world_writable_warning", + }, + ]); + const worldWritableContent = readEventPayloadString(events().at(-1)!, "content"); + expect(worldWritableContent).toContain("C:\\unsafe-one"); + expect(worldWritableContent).toContain("2 additional affected paths"); + expect(worldWritableContent).toContain("scan did not complete"); + await logger.destroy(); + }); }); diff --git a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts index 4c22871..69ec3da 100644 --- a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts +++ b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts @@ -1,90 +1,11 @@ import { describe, expect, test } from "bun:test"; -import type { AgentDriverContext } from "../src/core/agent-driver-backend"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; -import { createBufferedSinkLogger } from "../src/observability"; -import type { DriverEventInput } from "../src/protocol/events"; -import { isDriverId } from "../src/protocol/id"; -import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; -import { DRIVER_TEST_IDS, driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; - -interface EventBatch { - events: DriverEventInput[]; - reason: string; -} - -function readEventPayloadString(event: DriverEventInput, field: string): string | null { - const payload = event.payload; - - if (typeof payload !== "object" || payload === null || Array.isArray(payload)) { - return null; - } - - const value = (payload as Record)[field]; - return typeof value === "string" ? value : null; -} - -function readAssistantMessageId(events: readonly DriverEventInput[]): string { - for (const event of events) { - const messageId = - readEventPayloadString(event, "messageId") ?? - readEventPayloadString(event, "parentMessageId"); - - if (messageId !== null) { - expect(isDriverId(messageId)).toBe(true); - return messageId; - } - } - - throw new Error("Expected a platform assistant message ID."); -} - -function createHarness(options: { failNativeResumePublish?: boolean; holdReason?: string } = {}) { - const batches: EventBatch[] = []; - const heldPush = Promise.withResolvers(); - const releasePush = Promise.withResolvers(); - const logger = createBufferedSinkLogger({ - level: "debug", - service: "openai-app-server-event-bridge-test", - sink: async () => {}, - }); - const context: AgentDriverContext = createAgentDriverContext({ - eventSink: { - pushEvents: async () => ({ accepted: [] }), - }, - logger, - payload: bootPayload, - permission: { - request: async () => "allow_once", - }, - }); - const bridge = new OpenAiAppServerEventBridge({ - push: async (_context, reason, events) => { - batches.push({ events, reason }); - if (reason === options.holdReason) { - heldPush.resolve(); - await releasePush.promise; - } - if ( - options.failNativeResumePublish === true && - reason === "driver.openai.native_resume_ref.updated" - ) { - throw new Error("event sink unavailable"); - } - }, - requireThreadId: () => "thread-1", - }); - - return { - batches, - bridge, - context, - events: () => batches.flatMap((batch) => batch.events), - heldPush: heldPush.promise, - logger, - releasePush: releasePush.resolve, - }; -} +import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; +import { + createOpenAiBridgeHarness as createHarness, + readAssistantMessageId, + readEventPayloadString, +} from "./openai-app-server-event-bridge-fixture"; describe("OpenAi app-server event bridge", () => { test("publishes a lossless completed assistant snapshot", async () => { @@ -122,6 +43,165 @@ describe("OpenAi app-server event bridge", () => { expect(snapshot?.delivery).toBe("lossless"); }); + test("preserves final phase and memory citations on completed snapshots", async () => { + const { bridge, context, events, logger } = createHarness(); + const memoryCitation = { + entries: [{ lineEnd: 8, lineStart: 4, note: "Relevant context", path: "MEMORY.md" }], + threadIds: ["thread-memory"], + }; + + await bridge.handleNotification(context, "item/completed", { + item: { + id: "message-final", + memoryCitation, + phase: "final_answer", + text: "Final answer", + type: "agentMessage", + }, + threadId: "thread-1", + turnId: "turn-1", + }); + await logger.destroy(); + + expect(events().find((event) => event.kind === "message.added")).toMatchObject({ + payload: { memoryCitation, phase: "final" }, + }); + expect(events().find((event) => event.kind === "message.delta")?.payload).not.toHaveProperty( + "phase", + ); + }); + + test("selects only final-phase messages once a turn uses explicit phases", async () => { + const { bridge, context, events, logger } = createHarness(); + + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { + id: "turn-1", + items: [ + { id: "legacy", memoryCitation: null, phase: null, text: "Legacy", type: "agentMessage" }, + { + id: "final", + memoryCitation: null, + phase: "final_answer", + text: "Authoritative final", + type: "agentMessage", + }, + { + id: "commentary", + memoryCitation: null, + phase: "commentary", + text: "Later commentary", + type: "agentMessage", + }, + { + delivery: "async", + id: "async-progress", + memoryCitation: null, + phase: "final_answer", + text: "Asynchronous progress", + type: "agentMessage", + }, + ], + status: "completed", + }, + }); + await logger.destroy(); + + const allEvents = events(); + const finalSnapshot = allEvents.find( + (event) => + event.kind === "message.added" && + readEventPayloadString(event, "content") === "Authoritative final", + ); + const runCompleted = allEvents.find((event) => event.kind === "run.completed"); + + expect(finalSnapshot).toBeDefined(); + expect(runCompleted).toBeDefined(); + expect(readEventPayloadString(runCompleted!, "finalMessageId")).toBe( + readEventPayloadString(finalSnapshot!, "messageId"), + ); + expect(runCompleted!.payload).not.toHaveProperty("finalMessageText"); + }); + + test("keeps asynchronous progress out of legacy final selection", async () => { + const { bridge, context, events, logger } = createHarness(); + + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { + id: "turn-1", + items: [ + { + id: "legacy", + memoryCitation: null, + phase: null, + text: "Legacy final", + type: "agentMessage", + }, + { + delivery: "async", + id: "async-progress", + memoryCitation: null, + phase: "final_answer", + text: "Asynchronous progress", + type: "agentMessage", + }, + ], + status: "completed", + }, + }); + await logger.destroy(); + + const allEvents = events(); + const finalSnapshot = allEvents.find( + (event) => + event.kind === "message.added" && + readEventPayloadString(event, "content") === "Legacy final", + ); + const asyncSnapshot = allEvents.find( + (event) => + event.kind === "message.added" && + readEventPayloadString(event, "content") === "Asynchronous progress", + ); + const runCompleted = allEvents.find((event) => event.kind === "run.completed"); + + expect(readEventPayloadString(runCompleted!, "finalMessageId")).toBe( + readEventPayloadString(finalSnapshot!, "messageId"), + ); + expect(asyncSnapshot?.payload).toMatchObject({ phase: "commentary" }); + }); + + test("does not fall back to a legacy snapshot when explicit commentary exists", async () => { + const { bridge, context, events, logger } = createHarness(); + + for (const item of [ + { id: "legacy", memoryCitation: null, phase: null, text: "Legacy", type: "agentMessage" }, + { + id: "commentary", + memoryCitation: null, + phase: "commentary", + text: "Commentary", + type: "agentMessage", + }, + ]) { + await bridge.handleNotification(context, "item/completed", { + item, + threadId: "thread-1", + turnId: "turn-1", + }); + } + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, + }); + await logger.destroy(); + + const terminalPayload = events().find((event) => event.kind === "run.completed")?.payload; + expect(terminalPayload).not.toHaveProperty("finalMessageId"); + expect(terminalPayload).not.toHaveProperty("finalMessageText"); + }); + test("fails an active turn exactly once when the provider exits", async () => { const { bridge, context, events, logger } = createHarness(); const failure = new Error("app-server exited"); @@ -142,7 +222,7 @@ describe("OpenAi app-server event bridge", () => { }); await expect(bridge.failActiveTurns(context, failure)).resolves.toBe(true); - await expect(completion).rejects.toBe(failure); + await expect(completion).rejects.toThrow("app-server exited"); await expect(bridge.failActiveTurns(context, failure)).resolves.toBe(false); await logger.destroy(); @@ -176,6 +256,23 @@ describe("OpenAi app-server event bridge", () => { ]); }); + test("replays run start after delivery rejection before committing deduplication", async () => { + const { attempts, bridge, context, logger } = createHarness({ + failReasonOnce: "driver.openai.turn.started", + }); + const started = { runId: DRIVER_TEST_IDS.runId, turnId: "turn-1" } as const; + + await expect(bridge.publishRunStarted(context, started)).rejects.toThrow("first attempt"); + await bridge.publishRunStarted(context, started); + + const starts = attempts.flatMap(({ events }) => + events.filter((event) => event.kind === "run.started"), + ); + expect(starts).toHaveLength(2); + expect(starts[0]!.sourceEventId).toBe(starts[1]!.sourceEventId); + await logger.destroy(); + }); + test("final turn items do not duplicate already completed messages", async () => { const { bridge, context, events, logger } = createHarness(); @@ -213,15 +310,6 @@ describe("OpenAi app-server event bridge", () => { role: "agent", }, }, - { - delivery: "best_effort", - kind: "message.delta", - payload: { - contentDelta: "pong", - messageId: assistantMessageId, - role: "agent", - }, - }, { delivery: "lossless", kind: "message.added", @@ -289,6 +377,7 @@ describe("OpenAi app-server event bridge", () => { await bridge.handleNotification(context, "item/started", { item: { id: "artifact-tool", + status: "inProgress", type: "commandExecution", }, threadId: "thread-1", @@ -298,6 +387,7 @@ describe("OpenAi app-server event bridge", () => { item: { aggregatedOutput: "artifact 已创建。", id: "artifact-tool", + status: "completed", type: "commandExecution", }, threadId: "thread-1", @@ -331,6 +421,7 @@ describe("OpenAi app-server event bridge", () => { { aggregatedOutput: "artifact 已创建。", id: "artifact-tool", + status: "completed", type: "commandExecution", }, { @@ -435,7 +526,7 @@ describe("OpenAi app-server event bridge", () => { const finalMessageId = readEventPayloadString(runCompleted, "finalMessageId"); expect(finalMessageId).toBe(deltas.find((entry) => entry.contentDelta === "消息乙")?.messageId); - expect(readEventPayloadString(runCompleted, "finalMessageText")).toBe("消息乙"); + expect(runCompleted.payload).not.toHaveProperty("finalMessageText"); }); test("command output streams as tool result content", async () => { @@ -494,7 +585,6 @@ describe("OpenAi app-server event bridge", () => { { kind: "tool.call.updated", payload: { - content: "hello", messageId: assistantMessageId, rawOutput: "hello", status: "running", @@ -504,7 +594,6 @@ describe("OpenAi app-server event bridge", () => { { kind: "tool.call.updated", payload: { - content: " world", messageId: assistantMessageId, rawOutput: " world", status: "running", @@ -533,6 +622,21 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-1", }); + await bridge.handleNotification(context, "turn/plan/updated", { + explanation: null, + plan: [ + { + status: "completed", + step: "Inspect stream events", + }, + { + status: "completed", + step: "Patch bridge", + }, + ], + threadId: "thread-1", + turnId: "turn-1", + }); await logger.destroy(); expect(events()).toEqual([ @@ -553,6 +657,26 @@ describe("OpenAi app-server event bridge", () => { ], source: "driver", }, + sourceEventId: "openai.turn.plan:turn-1:0", + }, + { + kind: "plan.updated", + payload: { + entries: [ + { + content: "Inspect stream events", + priority: "medium", + status: "completed", + }, + { + content: "Patch bridge", + priority: "medium", + status: "completed", + }, + ], + source: "driver", + }, + sourceEventId: "openai.turn.plan:turn-1:1", }, ]); }); diff --git a/tests/openai-app-server-event-bridge-terminal.test.ts b/tests/openai-app-server-event-bridge-terminal.test.ts index 37f7ebb..e7abd7e 100644 --- a/tests/openai-app-server-event-bridge-terminal.test.ts +++ b/tests/openai-app-server-event-bridge-terminal.test.ts @@ -2,92 +2,264 @@ import { describe, expect, test } from "bun:test"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; +import type { AgentDriverPermissionPort, DriverPermissionRequest } from "../src/host-ports"; +import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; +import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; +import { + parseServerNotification, + parseServerRequest, +} from "../src/runtimes/openai/app-server-protocol-server"; +import { OpenAiAppServerRequestHandler } from "../src/runtimes/openai/app-server-request-handler"; +import { createCmaMemoryStore } from "../src/stores/memory"; import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; -import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; +import { driverBootPayload, driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; +import { + createOpenAiBridgeHarness as createHarness, + readAssistantMessageId, + readEventPayloadString, +} from "./openai-app-server-event-bridge-fixture"; -interface EventBatch { - events: DriverEventInput[]; - reason: string; -} - -function readEventPayloadString(event: DriverEventInput, field: string): string | null { - const payload = event.payload; - - if (typeof payload !== "object" || payload === null || Array.isArray(payload)) { - return null; - } - - const value = (payload as Record)[field]; - return typeof value === "string" ? value : null; -} - -function readAssistantMessageId(events: readonly DriverEventInput[]): string { - for (const event of events) { - const messageId = - readEventPayloadString(event, "messageId") ?? - readEventPayloadString(event, "parentMessageId"); - - if (messageId !== null) { - expect(isDriverId(messageId)).toBe(true); - return messageId; - } - } +function expectRunFinalReferences( + events: readonly DriverEventInput[], + expectedContent: string, +): void { + const terminal = events.find((event) => event.kind === "run.completed"); + const messageId = + terminal === undefined ? null : readEventPayloadString(terminal, "finalMessageId"); + const snapshots = events.filter( + (event) => + event.delivery === "lossless" && + readEventPayloadString(event, "messageId") === messageId && + (event.kind === "message.added" || event.kind === "message.delta"), + ); + const content = snapshots + .map((event) => + readEventPayloadString(event, event.kind === "message.added" ? "content" : "contentDelta"), + ) + .join(""); - throw new Error("Expected a platform assistant message ID."); + expect(terminal).toBeDefined(); + expect(messageId).not.toBeNull(); + expect(snapshots[0]?.kind).toBe("message.added"); + expect(content).toBe(expectedContent); + expect(terminal!.payload).not.toHaveProperty("finalMessageText"); } -function createHarness(options: { failNativeResumePublish?: boolean; holdReason?: string } = {}) { - const batches: EventBatch[] = []; - const heldPush = Promise.withResolvers(); - const releasePush = Promise.withResolvers(); +function createPublisherHarness( + options: { + partialSourcePrefix?: string; + requestPermission?: AgentDriverPermissionPort["request"]; + threadId?: string; + } = {}, +) { + const threadId = options.threadId ?? "thread-1"; + const delivered: DriverEventInput[] = []; + const partialAttempts: DriverEventInput[][] = []; + const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); + let nextSeq = 1; const logger = createBufferedSinkLogger({ level: "debug", - service: "openai-app-server-event-bridge-test", + service: "openai-app-server-event-bridge-publisher-test", sink: async () => {}, }); const context: AgentDriverContext = createAgentDriverContext({ eventSink: { - pushEvents: async () => ({ accepted: [] }), + currentRunId: () => DRIVER_TEST_IDS.runId, + pushEvents: async ({ events }) => { + const partialSourcePrefix = options.partialSourcePrefix; + const isPartialAttempt = + partialSourcePrefix !== undefined && + events[0]?.sourceEventId?.startsWith(partialSourcePrefix) === true; + + if (isPartialAttempt) { + partialAttempts.push(events); + if (partialAttempts.length === 2) { + throw new Error("transient snapshot delivery failure"); + } + } + + const acceptedEvents = + isPartialAttempt && partialAttempts.length === 1 ? events.slice(0, 1) : events; + for (const event of acceptedEvents) { + for (const envelope of toDriverEventEnvelopes( + driverBootPayload, + event, + DRIVER_TEST_IDS.runId, + )) { + await store.appendDriverEvent(DRIVER_TEST_IDS.sessionId, envelope.event); + } + } + delivered.push(...acceptedEvents); + return { + accepted: acceptedEvents.map((event) => ({ + eventId: event.sourceEventId, + seq: nextSeq++, + type: event.kind, + })), + }; + }, }, logger, payload: bootPayload, permission: { - request: async () => "allow_once", + request: options.requestPermission ?? (async () => "allow_once"), }, }); + const publisher = new DriverEventPublisher("openai-runtime", () => threadId); const bridge = new OpenAiAppServerEventBridge({ - push: async (_context, reason, events) => { - batches.push({ events, reason }); - if (reason === options.holdReason) { - heldPush.resolve(); - await releasePush.promise; - } - if ( - options.failNativeResumePublish === true && - reason === "driver.openai.native_resume_ref.updated" - ) { - throw new Error("event sink unavailable"); - } - }, - requireThreadId: () => "thread-1", + push: (pushContext, reason, events) => publisher.push(pushContext, reason, events), + pushSession: (pushContext, reason, events) => + publisher.pushSession(pushContext, reason, events), + pushTerminal: (pushContext, reason, closures, terminal) => + publisher.pushTerminal(pushContext, reason, closures, terminal), + requireThreadId: () => threadId, }); - return { - batches, - bridge, - context, - events: () => batches.flatMap((batch) => batch.events), - heldPush: heldPush.promise, - logger, - releasePush: releasePush.resolve, - }; + return { bridge, context, delivered, logger, partialAttempts }; } describe("OpenAi app-server event bridge", () => { + test("reports monotonic per-turn usage from cumulative thread totals", async () => { + const harness = createHarness(); + const trackedTurn = harness.bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const notify = async (last: readonly number[], total: readonly number[]) => { + const usage = (values: readonly number[]) => ({ + cacheWriteInputTokens: values[0]!, + cachedInputTokens: values[1]!, + inputTokens: values[2]!, + outputTokens: values[3]!, + reasoningOutputTokens: values[4]!, + totalTokens: values[5]!, + }); + + await harness.bridge.handleNotification(harness.context, "thread/tokenUsage/updated", { + threadId: "thread-1", + tokenUsage: { last: usage(last), modelContextWindow: 200_000, total: usage(total) }, + turnId: "turn-1", + }); + }; + + await notify([3, 2, 10, 4, 1, 14], [33, 22, 110, 54, 11, 164]); + await notify([4, 1, 3, 2, 1, 5], [37, 23, 113, 56, 12, 169]); + await notify([1, 0, 1, 1, 0, 2], [34, 22, 111, 55, 11, 165]); + + expect( + harness + .events() + .filter((event) => event.kind === "usage.updated") + .map((event) => event.payload), + ).toEqual([ + expect.objectContaining({ + cachedReadTokens: 2, + cachedWriteTokens: 3, + inputTokens: 10, + outputTokens: 4, + thoughtTokens: 1, + totalTokens: 14, + }), + expect.objectContaining({ + cachedReadTokens: 3, + cachedWriteTokens: 7, + inputTokens: 13, + outputTokens: 6, + thoughtTokens: 2, + totalTokens: 19, + }), + ]); + harness.bridge.rejectTurn("turn-1", new Error("test complete")); + await harness.logger.destroy(); + }); + + test("replays usage after delivery rejection before committing its baseline", async () => { + const harness = createHarness({ failReasonOnce: "driver.openai.usage.updated" }); + const trackedTurn = harness.bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const params = { + threadId: "thread-1", + tokenUsage: { + last: { + cacheWriteInputTokens: 0, + cachedInputTokens: 0, + inputTokens: 10, + outputTokens: 4, + reasoningOutputTokens: 1, + totalTokens: 14, + }, + modelContextWindow: 200_000, + total: { + cacheWriteInputTokens: 0, + cachedInputTokens: 0, + inputTokens: 10, + outputTokens: 4, + reasoningOutputTokens: 1, + totalTokens: 14, + }, + }, + turnId: "turn-1", + } as const; + + await expect( + harness.bridge.handleNotification(harness.context, "thread/tokenUsage/updated", params), + ).rejects.toThrow("first attempt"); + await harness.bridge.handleNotification(harness.context, "thread/tokenUsage/updated", params); + + expect(harness.events().filter((event) => event.kind === "usage.updated")).toHaveLength(1); + harness.bridge.rejectTurn("turn-1", new Error("test complete")); + await harness.logger.destroy(); + }); + + test("attributes cumulative usage growth across turns without replaying the previous total", async () => { + const harness = createHarness(); + const usage = (totalTokens: number) => ({ + cacheWriteInputTokens: 0, + cachedInputTokens: 0, + inputTokens: totalTokens, + outputTokens: 0, + reasoningOutputTokens: 0, + totalTokens, + }); + const notify = async (turnId: string, last: number, total: number) => { + await harness.bridge.handleNotification(harness.context, "thread/tokenUsage/updated", { + threadId: "thread-1", + tokenUsage: { + last: usage(last), + modelContextWindow: 200_000, + total: usage(total), + }, + turnId, + }); + }; + + await notify("resume-snapshot", 20, 100); + + const turn1 = harness.bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void turn1.catch(() => {}); + await notify("turn-1", 30, 130); + harness.bridge.rejectTurn("turn-1", new Error("turn complete")); + + const turn2 = harness.bridge.trackTurn("turn-2", DRIVER_TEST_IDS.runId); + void turn2.catch(() => {}); + await notify("turn-2", 30, 130); + await notify("turn-2", 30, 160); + + expect( + harness + .events() + .filter((event) => event.kind === "usage.updated") + .map((event) => event.payload), + ).toEqual([ + expect.objectContaining({ size: 200_000, totalTokens: 30, used: 30 }), + expect.objectContaining({ size: 200_000, totalTokens: 30, used: 30 }), + ]); + harness.bridge.rejectTurn("turn-2", new Error("test complete")); + await harness.logger.destroy(); + }); + test.each([ ["completed", "run.completed", true], ["failed", "run.failed", false], @@ -140,6 +312,7 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", tokenUsage: { last: { + cacheWriteInputTokens: 3, cachedInputTokens: 2, inputTokens: 10, outputTokens: 4, @@ -148,6 +321,7 @@ describe("OpenAi app-server event bridge", () => { }, modelContextWindow: 200_000, total: { + cacheWriteInputTokens: 3, cachedInputTokens: 2, inputTokens: 10, outputTokens: 4, @@ -165,6 +339,11 @@ describe("OpenAi app-server event bridge", () => { await harness.bridge.handleNotification(harness.context, "thread/tokenUsage/updated", usage); await harness.bridge.handleNotification(harness.context, "turn/diff/updated", diff); + expect(harness.events().find((event) => event.kind === "usage.updated")?.payload).toMatchObject( + { + cachedWriteTokens: 3, + }, + ); const completion = harness.bridge.handleNotification(harness.context, "turn/completed", { threadId: "thread-1", @@ -200,6 +379,407 @@ describe("OpenAi app-server event bridge", () => { await harness.logger.destroy(); }); + test("bounds a large turn diff before real CMA admission", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const diff = "x".repeat(1_100_000); + + const notification = parseServerNotification({ + method: "turn/diff/updated", + params: { diff, threadId: "thread-1", turnId: "turn-1" }, + }); + expect(notification).not.toBeNull(); + await bridge.handleNotification(context, notification!.method, notification!.params); + + const diagnostic = delivered.find( + (event) => + event.kind === "diagnostic.reported" && + readEventPayloadString(event, "message") === "OpenAI turn diff updated.", + ); + expect(diagnostic).toMatchObject({ + delivery: "best_effort", + payload: { details: { utf8Bytes: 1_100_000 } }, + }); + expect(JSON.stringify(diagnostic)).not.toContain(diff.slice(0, 1_024)); + bridge.rejectTurn("turn-1", new Error("test complete")); + await logger.destroy(); + }); + + test("bounds non-durable official telemetry before real CMA admission", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const large = "x".repeat(1_100_000); + const notifications = [ + parseServerNotification({ + method: "hook/completed", + params: { + run: { + completedAt: 2, + displayOrder: 0, + durationMs: 1, + entries: [{ kind: "context", text: large }], + eventName: "preToolUse", + executionMode: "sync", + handlerType: "command", + id: "hook-1", + scope: "turn", + source: "user", + sourcePath: "/tmp/hook", + startedAt: 1, + status: "completed", + statusMessage: null, + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }), + parseServerNotification({ + method: "item/autoApprovalReview/started", + params: { + action: { command: "echo ok", cwd: "/tmp", source: "shell", type: "command" }, + review: { rationale: large, status: "inProgress" }, + reviewId: "review-1", + startedAtMs: 1, + targetItemId: "tool-1", + threadId: "thread-1", + turnId: "turn-1", + }, + }), + parseServerNotification({ + method: "mcpServer/startupStatus/updated", + params: { + error: large, + failureReason: null, + name: "filesystem", + status: "failed", + threadId: "thread-1", + }, + }), + parseServerNotification({ + method: "model/rerouted", + params: { + fromModel: large, + reason: "highRiskCyberActivity", + threadId: "thread-1", + toModel: "gpt-b", + turnId: "turn-1", + }, + }), + parseServerNotification({ + method: "model/safetyBuffering/updated", + params: { + fasterModel: null, + model: "gpt-b", + reasons: ["policy"], + showBufferingUi: true, + threadId: "thread-1", + turnId: "turn-1", + useCases: [large], + }, + }), + ]; + expect(notifications.every((notification) => notification !== null)).toBe(true); + + for (const notification of notifications) { + await bridge.handleNotification(context, notification!.method, notification!.params); + } + + expect(delivered.map((event) => event.kind)).toEqual([ + "hook.completed", + "permission.review.started", + "mcp.server.updated", + "model.routing.updated", + "model.routing.updated", + ]); + expect(delivered.every((event) => event.delivery === "best_effort")).toBe(true); + expect(delivered.every((event) => Buffer.byteLength(JSON.stringify(event)) < 1_048_576)).toBe( + true, + ); + expect(JSON.stringify(delivered)).not.toContain(large); + await logger.destroy(); + }); + + test("maps official approval identities and bounds terminal telemetry before CMA", async () => { + const permission = Promise.withResolvers(); + const nativeThreadId = `thread-${"i".repeat(300)}`; + const { bridge, context, delivered, logger } = createPublisherHarness({ + requestPermission: async (input) => { + permission.resolve(input); + return "allow_once"; + }, + threadId: nativeThreadId, + }); + const nativeItemId = `command-${"i".repeat(300)}`; + const nativeMcpItemId = `mcp-${"i".repeat(300)}`; + const nativeTurnId = `turn-${"i".repeat(300)}`; + const large = "x".repeat(1_100_000); + const trackedTurn = bridge.trackTurn(nativeTurnId, DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const itemStarts = [ + parseServerNotification({ + method: "item/started", + params: { + item: { + aggregatedOutput: null, + command: "printf ok", + commandActions: [], + cwd: "/tmp", + durationMs: null, + exitCode: null, + id: nativeItemId, + pluginId: null, + processId: null, + scriptPath: null, + source: "agent", + status: "inProgress", + type: "commandExecution", + }, + startedAtMs: 1, + threadId: nativeThreadId, + turnId: nativeTurnId, + }, + }), + parseServerNotification({ + method: "item/started", + params: { + item: { + appContext: null, + arguments: {}, + durationMs: null, + error: null, + id: nativeMcpItemId, + pluginId: null, + readOnlyHint: null, + result: null, + server: "filesystem", + status: "inProgress", + tool: "read_file", + type: "mcpToolCall", + }, + startedAtMs: 1, + threadId: nativeThreadId, + turnId: nativeTurnId, + }, + }), + ]; + expect(itemStarts.every((notification) => notification !== null)).toBe(true); + for (const notification of itemStarts) { + await bridge.handleNotification(context, notification!.method, notification!.params); + } + + const request = parseServerRequest({ + id: 1, + method: "item/commandExecution/requestApproval", + params: { + environmentId: null, + itemId: nativeItemId, + startedAtMs: 2, + threadId: nativeThreadId, + turnId: nativeTurnId, + }, + }); + expect(request).not.toBeNull(); + const response = Promise.withResolvers(); + const errors: Error[] = []; + const handler = new OpenAiAppServerRequestHandler({ + context, + handleError: async (error) => { + errors.push(error); + }, + isStopped: () => false, + mapToolCallId: (toolCallId) => bridge.mapToolCallId(toolCallId), + respond: (_id, result) => response.resolve(result), + respondError: (_id, message) => errors.push(new Error(message)), + }); + handler.dispatch(request!.method, request!.id, request!.params); + await expect(response.promise).resolves.toEqual({ decision: "accept" }); + + const notifications = [ + parseServerNotification({ + method: "item/autoApprovalReview/started", + params: { + action: { command: "printf ok", cwd: "/tmp", source: "shell", type: "command" }, + review: { + rationale: null, + riskLevel: null, + status: "inProgress", + userAuthorization: null, + }, + reviewId: "review-1", + startedAtMs: 2, + targetItemId: nativeItemId, + threadId: nativeThreadId, + turnId: nativeTurnId, + }, + }), + parseServerNotification({ + method: "item/mcpToolCall/progress", + params: { + itemId: nativeMcpItemId, + message: large, + threadId: nativeThreadId, + turnId: nativeTurnId, + }, + }), + parseServerNotification({ + method: "item/commandExecution/terminalInteraction", + params: { + itemId: nativeItemId, + processId: large, + stdin: "", + threadId: nativeThreadId, + turnId: nativeTurnId, + }, + }), + parseServerNotification({ + method: "turn/diff/updated", + params: { + diff: "diff --git a/file b/file", + threadId: nativeThreadId, + turnId: nativeTurnId, + }, + }), + ]; + expect(notifications.every((notification) => notification !== null)).toBe(true); + for (const notification of notifications) { + await bridge.handleNotification(context, notification!.method, notification!.params); + } + + const publicItemId = bridge.mapToolCallId(nativeItemId); + const permissionInput = await permission.promise; + const review = delivered.find((event) => event.kind === "permission.review.started"); + const progress = delivered.find( + (event) => + event.kind === "tool.call.updated" && + typeof event.payload === "object" && + event.payload !== null && + !Array.isArray(event.payload) && + (event.payload as Record)["rawOutputUtf8Bytes"] === 1_100_000, + ); + const shell = delivered.find((event) => event.kind === "shell.command.updated"); + const diff = delivered.find( + (event) => + event.kind === "diagnostic.reported" && + readEventPayloadString(event, "message") === "OpenAI turn diff updated.", + ); + expect(isDriverId(publicItemId)).toBe(true); + expect(permissionInput.toolCallId).toBe(publicItemId); + expect(readEventPayloadString(review!, "targetItemId")).toBe(publicItemId); + expect(progress?.payload).toMatchObject({ + rawOutputUtf8Bytes: 1_100_000, + status: "running", + toolCallId: expect.any(String), + }); + expect(progress?.payload).not.toHaveProperty("rawOutput"); + expect(shell?.payload).toMatchObject({ + itemId: publicItemId, + processIdUtf8Bytes: 1_100_000, + status: "running", + }); + expect(shell?.payload).not.toHaveProperty("processId"); + expect(isDriverId(readEventPayloadString(shell!, "threadId"))).toBe(true); + expect(isDriverId(readEventPayloadString(shell!, "turnId"))).toBe(true); + expect(readEventPayloadString(diff!, "turnId")).toBe(readEventPayloadString(shell!, "turnId")); + expect(delivered.every((event) => Buffer.byteLength(JSON.stringify(event)) < 1_048_576)).toBe( + true, + ); + expect(errors).toEqual([]); + bridge.rejectTurn(nativeTurnId, new Error("test complete")); + await handler.abortAll(new Error("test complete")); + await logger.destroy(); + }); + + test("chunks large user-facing warnings and bounds world-writable samples for CMA", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const large = "x".repeat(1_100_000); + const notifications = [ + parseServerNotification({ + method: "warning", + params: { message: large, threadId: "thread-1" }, + }), + parseServerNotification({ + method: "guardianWarning", + params: { message: large, threadId: "thread-1" }, + }), + parseServerNotification({ + method: "autoApprovalReview/strictReviewRequired", + params: { startedAtMs: 1, threadId: "thread-1", turnId: "turn-1" }, + }), + parseServerNotification({ + method: "windows/worldWritableWarning", + params: { extraCount: 2, failedScan: false, samplePaths: [large] }, + }), + ]; + expect(notifications.every((notification) => notification !== null)).toBe(true); + + for (const notification of notifications) { + await bridge.handleNotification(context, notification!.method, notification!.params); + } + + for (const subtype of ["warning", "guardian_warning"] as const) { + const events = delivered.filter( + (event) => readEventPayloadString(event, "subtype") === subtype, + ); + expect(events.map((event) => readEventPayloadString(event, "content")).join("")).toBe(large); + expect(events.length).toBeGreaterThan(1); + expect(events.every((event) => event.delivery === "lossless")).toBe(true); + } + const strictReview = delivered.find( + (event) => readEventPayloadString(event, "subtype") === "strict_review_required", + ); + expect(strictReview?.delivery).toBe("lossless"); + expect(strictReview?.payload).toMatchObject({ startedAtMs: 1 }); + const worldWritable = delivered.find( + (event) => readEventPayloadString(event, "subtype") === "windows_world_writable_warning", + ); + expect(worldWritable?.payload).toMatchObject({ extraCount: 3, samplePaths: [] }); + expect(delivered.every((event) => Buffer.byteLength(JSON.stringify(event)) < 1_048_576)).toBe( + true, + ); + await logger.destroy(); + }); + + test("bounds an official failed-turn error before terminal CMA admission", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + const large = "x".repeat(1_100_000); + const notification = parseServerNotification({ + method: "turn/completed", + params: { + threadId: "thread-1", + turn: { + completedAt: 2, + durationMs: 1, + error: { additionalDetails: large, codexErrorInfo: null, message: large }, + id: "turn-1", + items: [], + itemsView: "notLoaded", + startedAt: 1, + status: "failed", + }, + }, + }); + expect(notification).not.toBeNull(); + + await bridge.handleNotification(context, notification!.method, notification!.params); + await expect(trackedTurn).rejects.toThrow("was omitted"); + + const terminal = delivered.at(-1); + expect(terminal).toMatchObject({ + kind: "run.failed", + payload: { + error: { + details: { + additionalDetailsUtf8Bytes: 1_100_000, + messageUtf8Bytes: 1_100_000, + }, + }, + }, + }); + expect(Buffer.byteLength(JSON.stringify(terminal))).toBeLessThan(1_048_576); + await logger.destroy(); + }); + test("removes OpenAI private citation markup from streamed and final assistant text", async () => { const { bridge, context, events, logger } = createHarness(); const privateCitation = "\uE200cite\uE202turn7search12\uE202turn8view0\uE201"; @@ -261,7 +841,7 @@ describe("OpenAi app-server event bridge", () => { expect(streamedText).toBe("beforeafter"); expect(runCompleted).toBeDefined(); - expect(readEventPayloadString(runCompleted!, "finalMessageText")).toBe("beforeafter"); + expectRunFinalReferences(allEvents, "beforeafter"); expect(diagnostics).toHaveLength(1); }); @@ -324,7 +904,7 @@ describe("OpenAi app-server event bridge", () => { } expect(readEventPayloadString(runCompleted, "finalMessageId")).toBeNull(); - expect(readEventPayloadString(runCompleted, "finalMessageText")).toBeNull(); + expect(runCompleted.payload).not.toHaveProperty("finalMessageText"); }); test("uses the last completed assistant snapshot when turn items are omitted", async () => { @@ -375,10 +955,7 @@ describe("OpenAi app-server event bridge", () => { throw new Error("Expected a run.completed event."); } - expect(readEventPayloadString(runCompleted, "finalMessageId")).not.toBeNull(); - expect(readEventPayloadString(runCompleted, "finalMessageText")).toBe( - "最终回答:中文 Markdown ✅", - ); + expectRunFinalReferences(events(), "最终回答:中文 Markdown ✅"); }); test("uses completed snapshots when terminal items are not loaded", async () => { @@ -389,6 +966,17 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-1", }); + await bridge.handleNotification(context, "item/completed", { + item: { + delivery: "async", + id: "message-async-progress", + phase: "final_answer", + text: "ASYNC PROGRESS", + type: "agentMessage", + }, + threadId: "thread-1", + turnId: "turn-1", + }); await bridge.handleNotification(context, "turn/completed", { threadId: "thread-1", turn: { @@ -406,7 +994,7 @@ describe("OpenAi app-server event bridge", () => { throw new Error("Expected a run.completed event."); } - expect(readEventPayloadString(runCompleted, "finalMessageText")).toBe("FINAL"); + expectRunFinalReferences(events(), "FINAL"); }); test("does not fall back when a full terminal item list has no assistant", async () => { @@ -434,7 +1022,7 @@ describe("OpenAi app-server event bridge", () => { throw new Error("Expected a run.completed event."); } - expect(readEventPayloadString(runCompleted, "finalMessageText")).toBeNull(); + expect(runCompleted.payload).not.toHaveProperty("finalMessageText"); }); test("uses first-seen item order when older completions arrive late", async () => { @@ -473,7 +1061,752 @@ describe("OpenAi app-server event bridge", () => { throw new Error("Expected a run.completed event."); } - expect(readEventPayloadString(runCompleted, "finalMessageText")).toBe("FINAL"); + expectRunFinalReferences(events(), "FINAL"); + }); + + test("publishes more than 31 open tool closures before the run terminal", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const completion = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + const toolCount = 40; + + for (let index = 0; index < toolCount; index += 1) { + await bridge.handleNotification(context, "item/started", { + item: { + id: `tool-${String(index)}`, + status: "inProgress", + type: "commandExecution", + }, + threadId: "thread-1", + turnId: "turn-1", + }); + } + + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { + id: "turn-1", + items: [], + itemsView: "notLoaded", + status: "completed", + }, + }); + await expect(completion).resolves.toBeUndefined(); + await logger.destroy(); + + expect( + delivered.filter( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "status") === "completed", + ), + ).toHaveLength(toolCount); + expect( + delivered.filter( + (event) => + event.kind === "item.completed" && + readEventPayloadString(event, "status") === "completed", + ), + ).toHaveLength(toolCount); + expect(delivered.filter((event) => event.kind === "run.completed")).toHaveLength(1); + expect(delivered.at(-1)?.kind).toBe("run.completed"); + }); + + test("publishes a final snapshot larger than the terminal byte limit by reference", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const completion = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + const finalText = "x".repeat(1_100_000); + + await bridge.handleNotification(context, "item/completed", { + item: { + id: "message-final", + text: finalText, + type: "agentMessage", + }, + threadId: "thread-1", + turnId: "turn-1", + }); + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { + id: "turn-1", + items: [], + itemsView: "notLoaded", + status: "completed", + }, + }); + await expect(completion).resolves.toBeUndefined(); + await logger.destroy(); + + expectRunFinalReferences(delivered, finalText); + expect(delivered.at(-1)?.kind).toBe("run.completed"); + }); + + test("commits a chunked snapshot once after a partial transport retry", async () => { + const sourcePrefix = "openai.item.completed:turn-1:message-partial:"; + const { bridge, context, delivered, logger, partialAttempts } = createPublisherHarness({ + partialSourcePrefix: sourcePrefix, + }); + const completion = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + const finalText = "x".repeat(1_100_000); + const notification = { + item: { + id: "message-partial", + text: finalText, + type: "agentMessage", + }, + threadId: "thread-1", + turnId: "turn-1", + }; + + await bridge.handleNotification(context, "item/completed", notification); + const deliveredAfterCompletion = delivered.length; + await bridge.handleNotification(context, "item/completed", notification); + expect(delivered).toHaveLength(deliveredAfterCompletion); + + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, + }); + await expect(completion).resolves.toBeUndefined(); + + expect(partialAttempts).toHaveLength(3); + expect(partialAttempts[2]?.map((event) => event.sourceEventId)).toEqual( + partialAttempts[1]?.map((event) => event.sourceEventId), + ); + const deliveredSnapshotIds = delivered + .filter((event) => event.sourceEventId?.startsWith(sourcePrefix) === true) + .map((event) => event.sourceEventId); + expect(new Set(deliveredSnapshotIds).size).toBe(deliveredSnapshotIds.length); + expect( + delivered.filter( + (event) => + event.kind === "message.completed" && + event.sourceEventId?.startsWith(sourcePrefix) === true, + ), + ).toHaveLength(1); + expectRunFinalReferences(delivered, finalText); + expect(delivered.at(-1)?.kind).toBe("run.completed"); + await logger.destroy(); + }); + + test("publishes a multi-byte final snapshot through CMA-safe lossless chunks", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const completion = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + const finalText = "界".repeat(400_000); + + await bridge.handleNotification(context, "item/completed", { + item: { + id: "message-final", + memoryCitation: { source: "memory-1" }, + phase: "final_answer", + text: finalText, + type: "agentMessage", + }, + threadId: "thread-1", + turnId: "turn-1", + }); + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { + id: "turn-1", + items: [], + itemsView: "notLoaded", + status: "completed", + }, + }); + await expect(completion).resolves.toBeUndefined(); + + expectRunFinalReferences(delivered, finalText); + const snapshot = delivered.find( + (event) => event.delivery === "lossless" && event.kind === "message.added", + ); + expect(snapshot?.payload).toMatchObject({ + memoryCitation: { source: "memory-1" }, + phase: "final", + }); + await logger.destroy(); + }); + + test("publishes a large command result once without exceeding CMA admission", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const output = "x".repeat(525_000); + + await bridge.handleNotification(context, "item/completed", { + item: { + aggregatedOutput: output, + command: "generate-output", + id: "tool-large", + status: "completed", + type: "commandExecution", + }, + threadId: "thread-1", + turnId: "turn-1", + }); + + const completion = delivered.find( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "toolCallId") === "tool-large" && + readEventPayloadString(event, "status") === "completed", + ); + expect(readEventPayloadString(completion!, "rawOutput")).toBe(output); + expect(completion?.payload).not.toHaveProperty("content"); + await logger.destroy(); + }); + + test("publishes a large dynamic result once through structured output", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const text = "x".repeat(600_000); + const notification = parseServerNotification({ + method: "item/completed", + params: { + completedAtMs: 1, + item: { + arguments: { query: "migration" }, + contentItems: [{ text, type: "inputText" }], + durationMs: 9, + id: "dynamic-large", + namespace: "project", + status: "completed", + success: true, + tool: "lookup", + type: "dynamicToolCall", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(notification).not.toBeNull(); + + await bridge.handleNotification(context, notification!.method, notification!.params); + + const completion = delivered.find( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "toolCallId") === "dynamic-large" && + readEventPayloadString(event, "status") === "completed", + ); + expect(completion?.payload).not.toHaveProperty("rawOutput"); + expect(completion?.payload).toMatchObject({ + structuredOutput: { contentItems: [{ text, type: "inputText" }] }, + }); + await logger.destroy(); + }); + + test("fails closed when one dynamic structured result exceeds CMA admission", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const notification = parseServerNotification({ + method: "item/completed", + params: { + completedAtMs: 1, + item: { + arguments: {}, + contentItems: [{ text: "x".repeat(1_100_000), type: "inputText" }], + durationMs: 9, + id: "dynamic-oversized", + namespace: "project", + status: "completed", + success: true, + tool: "lookup", + type: "dynamicToolCall", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(notification).not.toBeNull(); + let failure: Error | null = null; + + try { + await bridge.handleNotification(context, notification!.method, notification!.params); + } catch (error) { + failure = error instanceof Error ? error : new Error("dynamic completion failed"); + } + + expect(failure?.message).toContain("durable event capacity"); + await bridge.failActiveTurns(context, failure!); + await expect(trackedTurn).rejects.toThrow("durable event capacity"); + expect( + delivered.find( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "toolCallId") === "dynamic-oversized" && + readEventPayloadString(event, "status") === "completed", + ), + ).toBeUndefined(); + await logger.destroy(); + }); + + test("fails the active turn instead of claiming an oversized tool result completed", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const output = "x".repeat(1_100_000); + let failure: Error | null = null; + + try { + await bridge.handleNotification(context, "item/completed", { + item: { + aggregatedOutput: output, + command: "generate-output", + id: "tool-oversized", + status: "completed", + type: "commandExecution", + }, + threadId: "thread-1", + turnId: "turn-1", + }); + } catch (error) { + failure = error instanceof Error ? error : new Error("tool completion failed"); + } + + expect(failure).not.toBeNull(); + expect(failure?.message).toContain("UTF-8 bytes"); + await bridge.failActiveTurns(context, failure!); + await expect(trackedTurn).rejects.toThrow("durable event capacity"); + + expect( + delivered.find( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "toolCallId") === "tool-oversized" && + readEventPayloadString(event, "status") === "completed", + ), + ).toBeUndefined(); + expect( + delivered.find( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "toolCallId") === "tool-oversized" && + readEventPayloadString(event, "status") === "failed", + ), + ).toBeDefined(); + expect(delivered.find((event) => event.kind === "run.failed")?.payload).toMatchObject({ + error: { message: expect.stringContaining("UTF-8 bytes") }, + }); + await logger.destroy(); + }); + + test("budgets the first message chunk around large citation metadata", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + const text = "a".repeat(512 * 1_024); + const notification = parseServerNotification({ + method: "item/completed", + params: { + completedAtMs: 1, + item: { + id: "message-citation", + memoryCitation: { + entries: [ + { + lineEnd: 1, + lineStart: 0, + note: "x".repeat(600_000), + path: "/memory.md", + }, + ], + threadIds: ["memory-thread"], + }, + phase: "final_answer", + text, + type: "agentMessage", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(notification).not.toBeNull(); + + await bridge.handleNotification(context, notification!.method, notification!.params); + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, + }); + await expect(trackedTurn).resolves.toBeUndefined(); + expectRunFinalReferences(delivered, text); + expect( + delivered.filter( + (event) => + event.delivery === "lossless" && + (event.kind === "message.added" || event.kind === "message.delta"), + ).length, + ).toBeGreaterThan(1); + await logger.destroy(); + }); + + test("budgets snapshot chunks after adding a long provider item identity", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + const itemId = `message-${"i".repeat(100_000)}`; + const text = "a".repeat(512 * 1_024); + const notification = parseServerNotification({ + method: "item/completed", + params: { + completedAtMs: 1, + item: { + id: itemId, + memoryCitation: { + entries: [ + { + lineEnd: 1, + lineStart: 0, + note: "x".repeat(600_000), + path: "/memory.md", + }, + ], + threadIds: ["memory-thread"], + }, + phase: "final_answer", + text, + type: "agentMessage", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(notification).not.toBeNull(); + + await bridge.handleNotification(context, notification!.method, notification!.params); + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, + }); + await expect(trackedTurn).resolves.toBeUndefined(); + expectRunFinalReferences(delivered, text); + await logger.destroy(); + }); + + test("maps long provider reasoning identity before durable CMA delivery", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + const itemId = `reasoning-${"i".repeat(262_000)}`; + const text = "x".repeat(512 * 1_024); + const notification = parseServerNotification({ + method: "item/reasoning/summaryTextDelta", + params: { + delta: text, + itemId, + summaryIndex: 0, + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(notification).not.toBeNull(); + + await bridge.handleNotification(context, notification!.method, notification!.params); + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, + }); + await expect(trackedTurn).resolves.toBeUndefined(); + + const reasoningEvents = delivered.filter((event) => event.kind.startsWith("thought.")); + const thoughtIds = reasoningEvents.map((event) => readEventPayloadString(event, "thoughtId")); + expect(thoughtIds.every((thoughtId) => thoughtId !== null && isDriverId(thoughtId))).toBe(true); + expect(new Set(thoughtIds).size).toBe(1); + expect( + reasoningEvents + .filter((event) => event.kind === "thought.delta") + .map((event) => readEventPayloadString(event, "contentDelta")) + .join(""), + ).toBe(text); + expect(reasoningEvents.some((event) => event.sourceEventId?.includes(itemId) === true)).toBe( + false, + ); + await logger.destroy(); + }); + + test("maps long provider item and tool identities before durable CMA delivery", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + const contextItemId = `compact-${"i".repeat(525_000)}`; + const toolItemId = `tool-${"i".repeat(525_000)}`; + const notifications = [ + parseServerNotification({ + method: "item/completed", + params: { + completedAtMs: 1, + item: { id: contextItemId, type: "contextCompaction" }, + threadId: "thread-1", + turnId: "turn-1", + }, + }), + parseServerNotification({ + method: "item/completed", + params: { + completedAtMs: 2, + item: { + aggregatedOutput: "done", + command: "printf done", + commandActions: [], + cwd: "/tmp", + durationMs: 1, + exitCode: 0, + id: toolItemId, + pluginId: null, + processId: null, + scriptPath: null, + source: "agent", + status: "completed", + type: "commandExecution", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }), + ]; + expect(notifications.every((notification) => notification !== null)).toBe(true); + + for (const notification of notifications) { + await bridge.handleNotification(context, notification!.method, notification!.params); + } + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, + }); + await expect(trackedTurn).resolves.toBeUndefined(); + + const compactedId = readEventPayloadString( + delivered.find((event) => event.kind === "context.compacted")!, + "itemId", + ); + const toolIds = delivered + .filter( + (event) => + event.kind === "tool.call.updated" || + (event.kind === "item.started" && + readEventPayloadString(event, "itemType") === "tool_call") || + (event.kind === "item.completed" && + readEventPayloadString(event, "itemType") === "tool_call"), + ) + .map( + (event) => + readEventPayloadString(event, "toolCallId") ?? readEventPayloadString(event, "itemId"), + ); + expect(compactedId !== null && isDriverId(compactedId)).toBe(true); + expect(toolIds.every((toolId) => toolId !== null && isDriverId(toolId))).toBe(true); + expect(new Set(toolIds).size).toBe(1); + expect( + delivered.some( + (event) => + event.sourceEventId?.includes(contextItemId) === true || + event.sourceEventId?.includes(toolItemId) === true, + ), + ).toBe(false); + await logger.destroy(); + }); + + test("fails closed before CMA when one official plan update exceeds durable capacity", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const notification = parseServerNotification({ + method: "item/plan/delta", + params: { + delta: "x".repeat(1_050_000), + itemId: "plan-large", + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(notification).not.toBeNull(); + let failure: Error | null = null; + + try { + await bridge.handleNotification(context, notification!.method, notification!.params); + } catch (error) { + failure = error instanceof Error ? error : new Error("plan update failed"); + } + + expect(failure?.message).toContain("durable event capacity"); + await bridge.failActiveTurns(context, failure!); + await expect(trackedTurn).rejects.toThrow("durable event capacity"); + expect(delivered.some((event) => event.kind === "plan.updated")).toBe(false); + expect(delivered.at(-1)?.kind).toBe("run.failed"); + await logger.destroy(); + }); + + test.each([ + [ + "MCP server", + { + arguments: {}, + id: "mcp-server-large", + server: "s".repeat(1_050_000), + status: "inProgress", + tool: "inspect", + type: "mcpToolCall", + }, + ], + [ + "MCP tool", + { + arguments: {}, + id: "mcp-tool-large", + server: "filesystem", + status: "inProgress", + tool: "t".repeat(1_050_000), + type: "mcpToolCall", + }, + ], + [ + "dynamic tool", + { + arguments: {}, + id: "dynamic-tool-large", + status: "inProgress", + tool: "t".repeat(1_050_000), + type: "dynamicToolCall", + }, + ], + ] as const)("fails %s start before poisoning CMA terminal delivery", async (_label, item) => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const notification = parseServerNotification({ + method: "item/started", + params: { item, startedAtMs: 1, threadId: "thread-1", turnId: "turn-1" }, + }); + expect(notification).not.toBeNull(); + let failure: Error | null = null; + + try { + await bridge.handleNotification(context, notification!.method, notification!.params); + } catch (error) { + failure = error instanceof Error ? error : new Error("tool start failed"); + } + + expect(failure?.message).toContain("tool start exceeds durable event capacity"); + await expect(bridge.failActiveTurns(context, failure!)).resolves.toBe(true); + await expect(trackedTurn).rejects.toThrow("tool start exceeds durable event capacity"); + expect(delivered.some((event) => event.kind === "item.started")).toBe(false); + expect(delivered.some((event) => event.kind === "tool.call.updated")).toBe(false); + expect(delivered.at(-1)?.kind).toBe("run.failed"); + await logger.destroy(); + }); + + test("completes a large renamed file patch without duplicating its diff", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const diff = "x".repeat(1_050_000); + const change = { + diff, + kind: { move_path: "/tmp/renamed.txt", type: "update" }, + path: "/tmp/file.txt", + }; + const notification = parseServerNotification({ + method: "item/fileChange/patchUpdated", + params: { + changes: [change], + itemId: "patch-large", + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(notification).not.toBeNull(); + + await bridge.handleNotification(context, notification!.method, notification!.params); + const completion = parseServerNotification({ + method: "item/completed", + params: { + completedAtMs: 1, + item: { + changes: [change], + id: "patch-large", + status: "completed", + type: "fileChange", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(completion).not.toBeNull(); + await bridge.handleNotification(context, completion!.method, completion!.params); + + expect(delivered.find((event) => event.kind === "file.change.updated")?.payload).toMatchObject({ + changes: [ + { change: "delete", path: "/tmp/file.txt" }, + { change: "upsert", path: "/tmp/renamed.txt" }, + ], + }); + expect( + delivered.some( + (event) => + event.kind === "tool.call.updated" && readEventPayloadString(event, "rawOutput") === diff, + ), + ).toBe(false); + expect( + delivered.some( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "toolCallId") === "patch-large" && + readEventPayloadString(event, "status") === "completed", + ), + ).toBe(true); + expect( + delivered.some( + (event) => + event.kind === "item.completed" && + readEventPayloadString(event, "itemId") === "patch-large", + ), + ).toBe(true); + + await bridge.failActiveTurns(context, new Error("test complete")); + await expect(trackedTurn).rejects.toThrow("test complete"); + await logger.destroy(); + }); + + test("fails closed when citation metadata alone exceeds CMA snapshot capacity", async () => { + const { bridge, context, delivered, logger } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + void trackedTurn.catch(() => {}); + const notification = parseServerNotification({ + method: "item/completed", + params: { + completedAtMs: 1, + item: { + id: "message-citation", + memoryCitation: { + entries: [ + { + lineEnd: 1, + lineStart: 0, + note: "x".repeat(1_100_000), + path: "/memory.md", + }, + ], + threadIds: ["memory-thread"], + }, + phase: "final_answer", + text: "short", + type: "agentMessage", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }); + expect(notification).not.toBeNull(); + let failure: Error | null = null; + + try { + await bridge.handleNotification(context, notification!.method, notification!.params); + } catch (error) { + failure = error instanceof Error ? error : new Error("message completion failed"); + } + + expect(failure?.message).toContain("message snapshot message-citation"); + await bridge.failActiveTurns(context, failure!); + await expect(trackedTurn).rejects.toThrow("durable event capacity"); + expect(delivered.find((event) => event.kind === "message.added")).toBeUndefined(); + expect(delivered.find((event) => event.kind === "message.completed")).toBeUndefined(); + expect(delivered.find((event) => event.kind === "message.failed")).toBeDefined(); + expect(delivered.find((event) => event.kind === "run.failed")).toBeDefined(); + expect(JSON.stringify(delivered)).not.toContain("memory-thread"); + await logger.destroy(); }); test("completed turns app final items before run finish", async () => { @@ -510,15 +1843,6 @@ describe("OpenAi app-server event bridge", () => { role: "agent", }, }, - { - delivery: "best_effort", - kind: "message.delta", - payload: { - contentDelta: "pong", - messageId: assistantMessageId, - role: "agent", - }, - }, { delivery: "lossless", kind: "message.added", diff --git a/tests/openai-app-server-event-bridge-transcript.test.ts b/tests/openai-app-server-event-bridge-transcript.test.ts index de05cf5..2538078 100644 --- a/tests/openai-app-server-event-bridge-transcript.test.ts +++ b/tests/openai-app-server-event-bridge-transcript.test.ts @@ -1,91 +1,11 @@ import { describe, expect, test } from "bun:test"; -import type { AgentDriverContext } from "../src/core/agent-driver-backend"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; -import { createBufferedSinkLogger } from "../src/observability"; -import type { DriverEventInput } from "../src/protocol/events"; -import { isDriverId } from "../src/protocol/id"; -import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; -import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; - -interface EventBatch { - events: DriverEventInput[]; - reason: string; -} - -function readEventPayloadString(event: DriverEventInput, field: string): string | null { - const payload = event.payload; - - if (typeof payload !== "object" || payload === null || Array.isArray(payload)) { - return null; - } - - const value = (payload as Record)[field]; - return typeof value === "string" ? value : null; -} - -function readAssistantMessageId(events: readonly DriverEventInput[]): string { - for (const event of events) { - const messageId = - readEventPayloadString(event, "messageId") ?? - readEventPayloadString(event, "parentMessageId"); - - if (messageId !== null) { - expect(isDriverId(messageId)).toBe(true); - return messageId; - } - } - - throw new Error("Expected a platform assistant message ID."); -} - -function createHarness(options: { failNativeResumePublish?: boolean; holdReason?: string } = {}) { - const batches: EventBatch[] = []; - const heldPush = Promise.withResolvers(); - const releasePush = Promise.withResolvers(); - const logger = createBufferedSinkLogger({ - level: "debug", - service: "openai-app-server-event-bridge-test", - sink: async () => {}, - }); - const context: AgentDriverContext = createAgentDriverContext({ - eventSink: { - pushEvents: async () => ({ accepted: [] }), - }, - logger, - payload: bootPayload, - permission: { - request: async () => "allow_once", - }, - }); - const bridge = new OpenAiAppServerEventBridge({ - push: async (_context, reason, events) => { - batches.push({ events, reason }); - if (reason === options.holdReason) { - heldPush.resolve(); - await releasePush.promise; - } - if ( - options.failNativeResumePublish === true && - reason === "driver.openai.native_resume_ref.updated" - ) { - throw new Error("event sink unavailable"); - } - }, - requireThreadId: () => "thread-1", - }); - - return { - batches, - bridge, - context, - events: () => batches.flatMap((batch) => batch.events), - heldPush: heldPush.promise, - logger, - releasePush: releasePush.resolve, - }; -} +import { + createOpenAiBridgeHarness as createHarness, + readAssistantMessageId, + readEventPayloadString, +} from "./openai-app-server-event-bridge-fixture"; describe("OpenAi app-server event bridge", () => { test("closes only open item state when a mixed turn is cancelled", async () => { @@ -100,17 +20,22 @@ describe("OpenAi app-server event bridge", () => { turnId: "turn-1", }); await bridge.handleNotification(context, "item/started", { - item: { id: "tool-completed", type: "commandExecution" }, + item: { id: "tool-completed", status: "inProgress", type: "commandExecution" }, threadId: "thread-1", turnId: "turn-1", }); await bridge.handleNotification(context, "item/completed", { - item: { aggregatedOutput: "", id: "tool-completed", type: "commandExecution" }, + item: { + aggregatedOutput: "", + id: "tool-completed", + status: "completed", + type: "commandExecution", + }, threadId: "thread-1", turnId: "turn-1", }); await bridge.handleNotification(context, "item/started", { - item: { id: "tool-open", type: "commandExecution" }, + item: { id: "tool-open", status: "inProgress", type: "commandExecution" }, threadId: "thread-1", turnId: "turn-1", }); @@ -126,8 +51,8 @@ describe("OpenAi app-server event bridge", () => { ) .map((event) => readEventPayloadString(event, "status")); expect(toolStatuses("tool-completed")).toEqual(["running", "completed"]); - expect(toolStatuses("tool-open")).toEqual(["running", "failed"]); - expect(events().filter((event) => event.kind === "thought.completed")).toHaveLength(1); + expect(toolStatuses("tool-open")).toEqual(["running", "cancelled"]); + expect(events().filter((event) => event.kind === "thought.cancelled")).toHaveLength(1); const terminalEventCount = events().length; await bridge.handleNotification(context, "item/reasoning/summaryTextDelta", { @@ -142,6 +67,51 @@ describe("OpenAi app-server event bridge", () => { await logger.destroy(); }); + test("does not duplicate streamed reasoning in the completed snapshot", async () => { + const { bridge, context, events, logger } = createHarness(); + + await bridge.handleNotification(context, "item/reasoning/summaryTextDelta", { + delta: "First", + itemId: "reasoning-1", + summaryIndex: 0, + threadId: "thread-1", + turnId: "turn-1", + }); + await bridge.handleNotification(context, "item/reasoning/summaryPartAdded", { + itemId: "reasoning-1", + summaryIndex: 1, + threadId: "thread-1", + turnId: "turn-1", + }); + await bridge.handleNotification(context, "item/reasoning/summaryPartAdded", { + itemId: "reasoning-1", + summaryIndex: 1, + threadId: "thread-1", + turnId: "turn-1", + }); + await bridge.handleNotification(context, "item/reasoning/summaryTextDelta", { + delta: "Second", + itemId: "reasoning-1", + summaryIndex: 1, + threadId: "thread-1", + turnId: "turn-1", + }); + await bridge.handleNotification(context, "item/completed", { + item: { content: [], id: "reasoning-1", summary: ["First", "Second"], type: "reasoning" }, + threadId: "thread-1", + turnId: "turn-1", + }); + await logger.destroy(); + + expect( + events() + .filter((event) => event.kind === "thought.delta") + .map((event) => readEventPayloadString(event, "contentDelta")) + .join(""), + ).toBe("First\n\nSecond"); + expect(events().filter((event) => event.kind === "thought.completed")).toHaveLength(1); + }); + test("releases translation state between turns and ignores late terminal replay", async () => { const { bridge, context, events, logger } = createHarness(); @@ -283,6 +253,7 @@ describe("OpenAi app-server event bridge", () => { await bridge.handleNotification(context, "error", { error: { additionalDetails: "HTTP 502 from upstream.", + codexErrorInfo: { responseStreamDisconnected: { httpStatusCode: 502 } }, message: "Response stream disconnected.", }, threadId: "thread-1", @@ -298,6 +269,7 @@ describe("OpenAi app-server event bridge", () => { turn: { error: { additionalDetails: "HTTP 502 from upstream.", + codexErrorInfo: { responseStreamDisconnected: { httpStatusCode: 502 } }, message: "Response stream disconnected.", }, id: "turn-1", @@ -321,9 +293,15 @@ describe("OpenAi app-server event bridge", () => { payload: { error: { code: "openai.turn_failed", + details: { + additionalDetails: "HTTP 502 from upstream.", + codexErrorInfo: "responseStreamDisconnected", + httpStatusCode: 502, + }, message: "Response stream disconnected.\nHTTP 502 from upstream.", + retryable: true, }, - recoverable: false, + recoverable: true, }, }, ]); @@ -342,6 +320,7 @@ describe("OpenAi app-server event bridge", () => { turn: { error: { additionalDetails: null, + codexErrorInfo: "serverOverloaded", message: "The model returned an empty response.", }, id: "turn-1", @@ -356,8 +335,11 @@ describe("OpenAi app-server event bridge", () => { payload: { error: { code: "openai.turn_failed", + details: { codexErrorInfo: "serverOverloaded" }, message: "The model returned an empty response.", + retryable: false, }, + recoverable: false, }, }, ]); @@ -386,15 +368,6 @@ describe("OpenAi app-server event bridge", () => { role: "agent", }, }, - { - delivery: "best_effort", - kind: "message.delta", - payload: { - contentDelta: "pong", - messageId: assistantMessageId, - role: "agent", - }, - }, { delivery: "lossless", kind: "message.added", @@ -453,8 +426,15 @@ describe("OpenAi app-server event bridge", () => { throw new Error("Expected a run.completed event."); } - expect(readEventPayloadString(runCompleted, "finalMessageText")).toBe( - "完整最终回答:中文 Markdown ✅", + const finalSnapshot = events().find( + (event) => + event.kind === "message.added" && + readEventPayloadString(event, "content") === "完整最终回答:中文 Markdown ✅", + ); + expect(finalSnapshot).toBeDefined(); + expect(readEventPayloadString(runCompleted, "finalMessageId")).toBe( + readEventPayloadString(finalSnapshot!, "messageId"), ); + expect(runCompleted.payload).not.toHaveProperty("finalMessageText"); }); }); diff --git a/tests/openai-app-server-provider-fixtures.test.ts b/tests/openai-app-server-provider-fixtures.test.ts index 9e8c7cc..1e583e2 100644 --- a/tests/openai-app-server-provider-fixtures.test.ts +++ b/tests/openai-app-server-provider-fixtures.test.ts @@ -8,13 +8,10 @@ import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; import { + CLIENT_REQUEST_RESULT_PARSERS, isServerRequestMethod, - isServerNotificationMethod, - OPENAI_APP_SERVER_SCHEMA_VERSION, - parseClientRequestResult, - parseServerNotificationParams, -} from "../src/runtimes/openai/generated/app-server-protocol"; -import type { ServerNotificationMethod } from "../src/runtimes/openai/generated/app-server-protocol"; + parseServerNotification, +} from "../src/runtimes/openai/app-server-protocol"; import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; @@ -134,6 +131,13 @@ function normalizeBridgeValue(value: unknown, fieldName?: string): unknown { return ""; } + if (fieldName === "sourceEventId") { + return value + .split(":") + .map((part) => (isDriverId(part) ? "" : part)) + .join(":"); + } + return value; } @@ -166,7 +170,7 @@ function normalizeBridgeEvent(event: DriverEventInput): Record return normalized; } -function createHarness() { +function createHarness(input: { sharedRootPath?: string } = {}) { const batches: EventBatch[] = []; const logger = createBufferedSinkLogger({ level: "debug", @@ -175,17 +179,35 @@ function createHarness() { }); const context: AgentDriverContext = createAgentDriverContext({ eventSink: { + currentRunId: () => DRIVER_TEST_IDS.runId, pushEvents: async () => ({ accepted: [] }), }, logger, - payload: bootPayload, + payload: { + ...bootPayload, + execution: { + ...bootPayload.execution, + session: { + ...bootPayload.execution.session, + sharedRootPath: input.sharedRootPath ?? bootPayload.execution.session.sharedRootPath, + }, + }, + }, permission: { request: async () => "allow_once", }, }); + const push = async (_context: AgentDriverContext, reason: string, events: DriverEventInput[]) => { + batches.push({ events, reason }); + }; const bridge = new OpenAiAppServerEventBridge({ - push: async (_context, reason, events) => { - batches.push({ events, reason }); + push, + pushSession: push, + pushTerminal: async (pushContext, reason, closures, terminal) => { + for (const closure of closures) { + await push(pushContext, `${reason}.items`, [closure]); + } + await push(pushContext, reason, [terminal]); }, requireThreadId: () => "thread-1", }); @@ -204,20 +226,107 @@ async function dispatchProviderNotification( bridge: OpenAiAppServerEventBridge, context: AgentDriverContext, ): Promise { - if (!isServerNotificationMethod(input.method)) { - await bridge.handleNotification( - context, - input.method as ServerNotificationMethod, - input.params as never, - ); + const notification = parseServerNotification({ + method: input.method, + params: input.params, + }); + + if (notification === null) { return; } - await bridge.handleNotification( - context, - input.method, - parseServerNotificationParams(input.method, input.params), - ); + await bridge.handleNotification(context, notification.method, notification.params); +} + +function parseNotificationParams(method: string, params: unknown) { + const notification = parseServerNotification({ method, params }); + + if (notification === null) { + throw new Error(`Unknown OpenAI app-server notification ${method}.`); + } + + return notification.params; +} + +function createOfficialStatusToolItem( + type: + | "collabAgentToolCall" + | "commandExecution" + | "dynamicToolCall" + | "fileChange" + | "imageGeneration" + | "mcpToolCall", + status: "failed" | "inProgress" | "in_progress", + id: string, +): Record { + switch (type) { + case "commandExecution": + return { + aggregatedOutput: null, + command: "true", + commandActions: [], + cwd: "/workspace", + durationMs: null, + exitCode: status === "failed" ? 1 : null, + id, + pluginId: null, + processId: null, + scriptPath: null, + source: "agent", + status, + type, + }; + case "fileChange": + return { changes: [], id, status, type }; + case "mcpToolCall": + return { + appContext: null, + arguments: {}, + durationMs: null, + error: status === "failed" ? { message: "failed" } : null, + id, + pluginId: null, + readOnlyHint: null, + result: null, + server: "test", + status, + tool: "inspect", + type, + }; + case "dynamicToolCall": + return { + arguments: {}, + contentItems: null, + durationMs: null, + id, + namespace: null, + status, + success: status === "failed" ? false : null, + tool: "inspect", + type, + }; + case "collabAgentToolCall": + return { + agentsStates: {}, + id, + model: null, + prompt: null, + reasoningEffort: null, + receiverThreadIds: [], + senderThreadId: "thread-1", + status, + tool: "wait", + type, + }; + case "imageGeneration": + return { + id, + result: "", + revisedPrompt: null, + status, + type, + }; + } } async function assertTrackTurnFixture( @@ -238,11 +347,79 @@ async function assertTrackTurnFixture( await expect(result).resolves.toBeUndefined(); } +function createThreadFixture(id = "thread-1") { + return { + agentNickname: null, + agentRole: null, + canAcceptDirectInput: true, + cliVersion: "0.150.1", + createdAt: 1_700_000_000, + cwd: "/workspace", + ephemeral: false, + extra: null, + forkedFromId: null, + gitInfo: null, + historyMode: "paginated", + id, + modelProvider: "openai", + name: null, + parentThreadId: null, + path: null, + preview: "Hello", + projectId: null, + recencyAt: null, + section: null, + sectionEnteredAt: null, + sessionId: "session-1", + source: "appServer", + status: { type: "idle" }, + threadSource: null, + turns: [], + updatedAt: 1_700_000_001, + } as const; +} + +function createThreadStartFixture() { + return { + activePermissionProfile: null, + approvalPolicy: "on-request", + approvalsReviewer: "user", + cwd: "/workspace", + instructionSources: ["/workspace/AGENTS.md"], + model: "gpt-5.6", + modelProvider: "openai", + multiAgentMode: "explicitRequestOnly", + reasoningEffort: "high", + runtimeWorkspaceRoots: ["/workspace"], + sandbox: { + excludeSlashTmp: false, + excludeTmpdirEnvVar: false, + networkAccess: false, + type: "workspaceWrite", + writableRoots: ["/workspace"], + }, + serviceTier: null, + thread: createThreadFixture(), + } as const; +} + +function createTurnFixture() { + return { + completedAt: null, + durationMs: null, + error: null, + id: "turn-1", + items: [], + itemsView: "full", + startedAt: 1_700_000_002, + status: "inProgress", + } as const; +} + describe("OpenAI app-server provider fixtures", () => { - test("matches the 0.144.5 reasoning, MCP progress, and interactive request surface", () => { - expect(OPENAI_APP_SERVER_SCHEMA_VERSION).toBe("0.144.5"); + test("matches the installed reasoning, MCP progress, and interactive request surface", () => { expect( - parseServerNotificationParams("item/reasoning/summaryPartAdded", { + parseNotificationParams("item/reasoning/summaryPartAdded", { itemId: "reasoning-1", summaryIndex: 1, threadId: "thread-1", @@ -255,7 +432,7 @@ describe("OpenAI app-server provider fixtures", () => { turnId: "turn-1", }); expect( - parseServerNotificationParams("item/mcpToolCall/progress", { + parseNotificationParams("item/mcpToolCall/progress", { itemId: "tool-1", message: "Working", threadId: "thread-1", @@ -271,55 +448,324 @@ describe("OpenAI app-server provider fixtures", () => { expect(isServerRequestMethod("item/tool/call")).toBe(true); expect(isServerRequestMethod("mcpServer/elicitation/request")).toBe(true); expect(isServerRequestMethod("account/chatgptAuthTokens/refresh")).toBe(true); + expect(isServerRequestMethod("applyPatchApproval")).toBe(true); expect(isServerRequestMethod("attestation/generate")).toBe(true); expect(isServerRequestMethod("currentTime/read")).toBe(true); + expect(isServerRequestMethod("execCommandApproval")).toBe(true); + expect(parseServerNotification({ method: "account/updated", params: {} })).toEqual({ + method: "account/updated", + params: {}, + }); expect( - parseServerNotificationParams("serverRequest/resolved", { + parseNotificationParams("serverRequest/resolved", { requestId: 7, threadId: "thread-1", }), ).toEqual({ requestId: 7, threadId: "thread-1" }); + expect( + parseNotificationParams("thread/tokenUsage/updated", { + threadId: "thread-1", + tokenUsage: { + last: { + cacheWriteInputTokens: 3, + cachedInputTokens: 2, + inputTokens: 10, + outputTokens: 4, + reasoningOutputTokens: 1, + totalTokens: 14, + }, + modelContextWindow: 200_000, + total: { + cacheWriteInputTokens: 30, + cachedInputTokens: 20, + inputTokens: 100, + outputTokens: 40, + reasoningOutputTokens: 10, + totalTokens: 140, + }, + }, + turnId: "turn-1", + }), + ).toEqual({ + threadId: "thread-1", + tokenUsage: { + last: { + cacheWriteInputTokens: 3, + cachedInputTokens: 2, + inputTokens: 10, + outputTokens: 4, + reasoningOutputTokens: 1, + totalTokens: 14, + }, + modelContextWindow: 200_000, + total: { + cacheWriteInputTokens: 30, + cachedInputTokens: 20, + inputTokens: 100, + outputTokens: 40, + reasoningOutputTokens: 10, + totalTokens: 140, + }, + }, + turnId: "turn-1", + }); }); - test.each([undefined, "inProgress", "unknown"])( + test("classifies unused MCP-stream and realtime timeline notifications explicitly", async () => { + const { bridge, context, events, logger } = createHarness(); + const realtimeItem = { + id: "realtime-item-1", + realtimeSessionId: "realtime-1", + type: "realtimeSessionStarted", + } as const; + const notifications: ProviderNotificationFixture[] = [ + { + method: "mcpServer/event/stream/notification", + params: { + notification: { method: "notifications/tools/list_changed", params: {} }, + subscriptionId: "subscription-1", + }, + }, + { + method: "thread/realtime/item/started", + params: { item: realtimeItem, threadId: "thread-1" }, + }, + { + method: "thread/realtime/item/transcript/delta", + params: { delta: "hello", itemId: realtimeItem.id, threadId: "thread-1" }, + }, + { + method: "thread/realtime/item/completed", + params: { item: realtimeItem, threadId: "thread-1" }, + }, + ]; + + for (const notification of notifications) { + expect(parseServerNotification(notification)).not.toBeNull(); + await dispatchProviderNotification(notification, bridge, context); + } + + expect(events()).toEqual([]); + await logger.destroy(); + }); + + test.each([undefined, "unknown", "inProgress"])( "rejects non-terminal turn/completed status %p", (status) => { expect(() => - parseServerNotificationParams("turn/completed", { + parseNotificationParams("turn/completed", { threadId: "thread-1", - turn: { id: "turn-1", ...(status === undefined ? {} : { status }) }, + turn: { id: "turn-1", items: [], ...(status === undefined ? {} : { status }) }, }), ).toThrow(); }, ); test.each([ - "completedAt", - "durationMs", - "error", - "items", - "itemsView", - "startedAt", - "status", - ] as const)("rejects turn/start without required Turn field %s", (missing) => { - const turn: Record = { - completedAt: null, - durationMs: null, - error: null, - id: "turn-1", - items: [], - itemsView: "notLoaded", - startedAt: null, - status: "inProgress", + ["failed", null], + ["completed", { message: "unexpected" }], + ["interrupted", { message: "unexpected" }], + ] as const)("rejects turn/completed status %s with contradictory error", (status, error) => { + expect(() => + parseNotificationParams("turn/completed", { + threadId: "thread-1", + turn: { error, id: "turn-1", items: [], status }, + }), + ).toThrow("turn.error must be present exactly when the turn failed"); + }); + + test("accepts a failed turn/completed with its required error", () => { + expect( + parseNotificationParams("turn/completed", { + threadId: "thread-1", + turn: { error: { message: "failed" }, id: "turn-1", items: [], status: "failed" }, + }), + ).toMatchObject({ turn: { error: { message: "failed" }, status: "failed" } }); + }); + + test.each(["id", "items", "status"] as const)( + "rejects turn/start without schema-required Turn field %s", + (missing) => { + const turn: Record = { + completedAt: null, + durationMs: null, + error: null, + id: "turn-1", + items: [], + itemsView: "notLoaded", + startedAt: null, + status: "inProgress", + }; + delete turn[missing]; + + expect(() => CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ turn })).toThrow(missing); + }, + ); + + test.each(["codexHome", "platformFamily", "platformOs", "userAgent"] as const)( + "rejects initialize without required response field %s", + (missing) => { + const response: Record = { + codexHome: "/tmp/openai-home", + platformFamily: "unix", + platformOs: "linux", + userAgent: "test-app-server/0.150.1", + }; + delete response[missing]; + + expect(() => CLIENT_REQUEST_RESULT_PARSERS.initialize(response)).toThrow(missing); + }, + ); + + test("preserves the complete initialize response", () => { + const response = { + codexHome: "/tmp/openai-home", + platformFamily: "unix", + platformOs: "linux", + userAgent: "test-app-server/0.150.1", + }; + + expect(CLIENT_REQUEST_RESULT_PARSERS.initialize(response)).toEqual(response); + }); + + test.each([ + "approvalPolicy", + "approvalsReviewer", + "cwd", + "model", + "modelProvider", + "sandbox", + "thread", + ] as const)("rejects thread/start without schema-required response field %s", (missing) => { + const response: Record = { ...createThreadStartFixture() }; + delete response[missing]; + + expect(() => CLIENT_REQUEST_RESULT_PARSERS["thread/start"](response)).toThrow(missing); + }); + + test("preserves the complete thread/start response and rejects a sparse thread", () => { + const response = createThreadStartFixture(); + + expect(CLIENT_REQUEST_RESULT_PARSERS["thread/start"](response)).toEqual(response); + expect(() => + CLIENT_REQUEST_RESULT_PARSERS["thread/start"]({ + ...response, + thread: { id: "thread-1" }, + }), + ).toThrow("sessionId"); + }); + + test.each(["thread/start", "thread/resume"] as const)( + "preserves omitted optional %s wire fields", + (method: "thread/resume" | "thread/start") => { + const response: Record = { + ...createThreadStartFixture(), + ...(method === "thread/resume" + ? { + initialTurnsPage: null, + itemsBackwardsCursor: null, + turnsBackwardsCursor: null, + } + : {}), + }; + delete response["reasoningEffort"]; + delete response["serviceTier"]; + + expect(CLIENT_REQUEST_RESULT_PARSERS[method](response)).toEqual(response); + }, + ); + + test("applies the official thread/resume pagination defaults", () => { + const response = createThreadStartFixture(); + + expect(CLIENT_REQUEST_RESULT_PARSERS["thread/resume"](response)).toEqual({ + ...response, + initialTurnsPage: null, + itemsBackwardsCursor: null, + turnsBackwardsCursor: null, + }); + }); + + test("preserves the complete thread/resume pagination response", () => { + const response = { + ...createThreadStartFixture(), + initialTurnsPage: { + backwardsCursor: "turn-head", + data: [createTurnFixture()], + nextCursor: null, + }, + itemsBackwardsCursor: "item-head", + turnsBackwardsCursor: "turn-head", }; - delete turn[missing]; - expect(() => parseClientRequestResult("turn/start", { turn })).toThrow(missing); + expect(CLIENT_REQUEST_RESULT_PARSERS["thread/resume"](response)).toEqual(response); + }); + + test("preserves structured turn errors and rejects unknown thread items", () => { + const turn = { + ...createTurnFixture(), + error: { + additionalDetails: "HTTP 502 from upstream.", + codexErrorInfo: { responseStreamDisconnected: { httpStatusCode: 502 } }, + message: "Response stream disconnected.", + }, + status: "failed", + }; + + expect(CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ turn })).toEqual({ turn }); + expect(() => + CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ + turn: { ...createTurnFixture(), items: [{ id: "item-1", type: "futureItem" }] }, + }), + ).toThrow("Invalid input"); + }); + + test.each([ + ["failed", null], + ["completed", { message: "unexpected" }], + ["interrupted", { message: "unexpected" }], + ["inProgress", { message: "unexpected" }], + ] as const)("rejects turn/start status %s with contradictory error", (status, error) => { + expect(() => + CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ + turn: { error, id: "turn-1", items: [], status }, + }), + ).toThrow("turn.error must be present exactly when the turn failed"); + }); + + test("accepts an in-progress turn/start without an error", () => { + expect( + CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ + turn: { id: "turn-1", items: [], status: "inProgress" }, + }), + ).toMatchObject({ turn: { status: "inProgress" } }); + }); + + test("accepts omitted optional TurnError wire fields without inventing classification", () => { + expect( + parseNotificationParams("error", { + error: { message: "Provider failed." }, + threadId: "thread-1", + turnId: "turn-1", + willRetry: false, + }), + ).toEqual({ + error: { additionalDetails: null, message: "Provider failed." }, + threadId: "thread-1", + turnId: "turn-1", + willRetry: false, + }); + }); + + test("rejects a non-object thread/inject_items response", () => { + expect(() => CLIENT_REQUEST_RESULT_PARSERS["thread/inject_items"](null)).toThrow( + "expected object", + ); }); test("preserves the assistant item identity on text deltas", () => { expect( - parseServerNotificationParams("item/agentMessage/delta", { + parseNotificationParams("item/agentMessage/delta", { delta: "中文 delta", itemId: "message-1", threadId: "thread-1", @@ -332,7 +778,7 @@ describe("OpenAI app-server provider fixtures", () => { turnId: "turn-1", }); expect(() => - parseServerNotificationParams("item/agentMessage/delta", { + parseNotificationParams("item/agentMessage/delta", { delta: "missing identity", threadId: "thread-1", turnId: "turn-1", @@ -342,7 +788,7 @@ describe("OpenAI app-server provider fixtures", () => { test("preserves the terminal turn item loading state", () => { expect( - parseServerNotificationParams("turn/completed", { + parseNotificationParams("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", @@ -361,6 +807,648 @@ describe("OpenAI app-server provider fixtures", () => { }); }); + test("maps official collaboration items to complete tool lifecycles", async () => { + const { bridge, context, events, logger } = createHarness(); + const cases = [ + { + agentStatus: "completed", + id: "collab-ok", + message: "result", + publicStatus: "completed", + status: "completed", + }, + { + agentStatus: "errored", + id: "collab-failed", + message: "boom", + publicStatus: "failed", + status: "failed", + }, + { + agentStatus: "interrupted", + id: "collab-interrupted", + message: "stopped", + publicStatus: "cancelled", + status: "interrupted", + }, + ] as const; + + for (const item of cases) { + await dispatchProviderNotification( + { + method: "item/started", + params: { + item: { + agentsStates: {}, + id: item.id, + model: "gpt-5.4", + prompt: "Inspect the migration", + reasoningEffort: "high", + receiverThreadIds: ["agent-1"], + senderThreadId: "thread-1", + status: "inProgress", + tool: "wait", + type: "collabAgentToolCall", + }, + startedAtMs: 1, + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ); + await dispatchProviderNotification( + { + method: "item/completed", + params: { + completedAtMs: 2, + item: { + agentsStates: { + "agent-1": { message: item.message, status: item.agentStatus }, + }, + id: item.id, + model: "gpt-5.4", + prompt: "Inspect the migration", + reasoningEffort: "high", + receiverThreadIds: ["agent-1"], + senderThreadId: "thread-1", + status: item.status, + tool: "wait", + type: "collabAgentToolCall", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ); + } + + for (const item of cases) { + const updates = events().filter( + (event) => + event.kind === "tool.call.updated" && + isRecord(event.payload) && + event.payload["toolCallId"] === item.id, + ); + + expect( + updates.map((event) => (isRecord(event.payload) ? event.payload["status"] : null)), + ).toEqual(["running", item.publicStatus]); + expect(updates.at(-1)).toMatchObject({ + payload: { + agentId: "agent-1", + structuredOutput: { + agentsStates: { + "agent-1": { message: item.message, status: item.agentStatus }, + }, + model: "gpt-5.4", + prompt: "Inspect the migration", + reasoningEffort: "high", + receiverThreadIds: ["agent-1"], + senderThreadId: "thread-1", + status: item.status, + tool: "wait", + }, + }, + }); + } + + await logger.destroy(); + }); + + test.each([ + ["sendMessage", "Send message to agent"], + ["followupTask", "Follow up with agent"], + ["interruptAgent", "Interrupt agent"], + ["listAgents", "List agents"], + ] as const)("maps the new %s collaboration tool lifecycle", async (tool, title) => { + const { bridge, context, events, logger } = createHarness(); + const baseItem = { + agentsStates: {}, + id: `collab-${tool}`, + model: null, + prompt: null, + reasoningEffort: null, + receiverThreadIds: [], + senderThreadId: "thread-1", + tool, + type: "collabAgentToolCall", + } as const; + + for (const [method, status] of [ + ["item/started", "inProgress"], + ["item/completed", "completed"], + ] as const) { + await dispatchProviderNotification( + { + method, + params: { + ...(method === "item/started" ? { startedAtMs: 1 } : { completedAtMs: 2 }), + item: { ...baseItem, status }, + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ); + } + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "tool.call.updated", + payload: expect.objectContaining({ status: "running", title }), + }), + ); + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "tool.call.updated", + payload: expect.objectContaining({ status: "completed" }), + }), + ); + await logger.destroy(); + }); + + test("maps official sleep items to tool lifecycle", async () => { + const { bridge, context, events, logger } = createHarness(); + const item = { durationMs: 2_000, id: "sleep-1", type: "sleep" } as const; + + for (const method of ["item/started", "item/completed"] as const) { + await dispatchProviderNotification( + { + method, + params: { + ...(method === "item/started" ? { startedAtMs: 1 } : { completedAtMs: 2 }), + item, + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ); + } + + const updates = events().filter( + (event) => + event.kind === "tool.call.updated" && + isRecord(event.payload) && + event.payload["toolCallId"] === item.id, + ); + expect( + updates.map((event) => (isRecord(event.payload) ? event.payload["status"] : null)), + ).toEqual(["running", "completed"]); + expect(updates.at(-1)).toMatchObject({ payload: { rawOutput: "Slept for 2000 ms." } }); + await logger.destroy(); + }); + + test("preserves command, MCP, and dynamic tool inputs and structured results", async () => { + const { bridge, context, events, logger } = createHarness(); + const items = [ + { + aggregatedOutput: "ok\n", + command: "bun test", + commandActions: [{ command: "bun test", type: "unknown" }], + cwd: "/workspace", + durationMs: 42, + exitCode: 0, + id: "command-audit", + pluginId: "plugin.test", + processId: "process-1", + scriptPath: "scripts/test.ts", + source: "agent", + status: "completed", + type: "commandExecution", + }, + { + appContext: null, + arguments: { depth: 2, path: "src" }, + durationMs: 7, + error: null, + id: "mcp-audit", + pluginId: null, + readOnlyHint: true, + result: { + _meta: { private: true }, + content: [{ _meta: { blockPrivate: true }, text: "done", type: "text" }], + structuredContent: { + _meta: { structuredPrivate: true }, + files: ["src/index.ts"], + }, + }, + server: "filesystem", + status: "completed", + tool: "inspect", + type: "mcpToolCall", + }, + { + arguments: { query: "migration" }, + contentItems: [{ text: "found", type: "inputText" }], + durationMs: 9, + id: "dynamic-audit", + namespace: "project", + status: "completed", + success: true, + tool: "lookup", + type: "dynamicToolCall", + }, + ] as const; + + for (const item of items) { + await dispatchProviderNotification( + { + method: "item/completed", + params: { completedAtMs: 2, item, threadId: "thread-1", turnId: "turn-1" }, + }, + bridge, + context, + ); + } + + const terminalPayload = (toolCallId: string) => + events().find( + (event) => + event.kind === "tool.call.updated" && + isRecord(event.payload) && + event.payload["toolCallId"] === toolCallId && + event.payload["status"] === "completed", + )?.payload; + expect(terminalPayload("command-audit")).toMatchObject({ + rawInput: "bun test", + rawOutput: "ok\n", + structuredOutput: { + commandActions: [{ command: "bun test", type: "unknown" }], + cwd: "/workspace", + durationMs: 42, + exitCode: 0, + pluginId: "plugin.test", + processId: "process-1", + scriptPath: "scripts/test.ts", + source: "agent", + }, + }); + expect(terminalPayload("mcp-audit")).toMatchObject({ + rawInput: '{"depth":2,"path":"src"}', + rawOutput: '[{"text":"done","type":"text"}]', + structuredOutput: { files: ["src/index.ts"] }, + }); + expect(JSON.stringify(terminalPayload("mcp-audit"))).not.toContain("_meta"); + expect(JSON.stringify(terminalPayload("mcp-audit"))).not.toContain("private"); + expect(terminalPayload("dynamic-audit")).toMatchObject({ + rawInput: '{"query":"migration"}', + structuredOutput: { + contentItems: [{ text: "found", type: "inputText" }], + durationMs: 9, + namespace: "project", + success: true, + }, + }); + await logger.destroy(); + }); + + test("fails closed when no durable image transport is available", async () => { + const pngBase64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; + const { bridge, context, events, logger } = createHarness(); + + await expect( + dispatchProviderNotification( + { + method: "item/completed", + params: { + completedAtMs: 2, + item: { + id: "image-outside", + result: pngBase64, + revisedPrompt: null, + savedPath: "/untrusted/provider.png", + status: "completed", + type: "imageGeneration", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ), + ).rejects.toThrow("without a supported durable image transport"); + expect(JSON.stringify(events())).not.toContain("/untrusted/provider.png"); + await logger.destroy(); + }); + + test("maps the remaining official ThreadItem variants without silent data loss", async () => { + const { bridge, context, events, logger } = createHarness(); + const completedItems = [ + { + action: { queries: ["OpenAI app-server 0.147"], query: null, type: "search" }, + id: "web-1", + query: "OpenAI app-server 0.147", + results: [{ title: "Protocol release", url: "https://example.test/release" }], + type: "webSearch", + }, + { id: "image-view-1", path: "/tmp/image.png", type: "imageView" }, + { id: "review-enter-1", review: "Review changes", type: "enteredReviewMode" }, + { id: "review-exit-1", review: "Review complete", type: "exitedReviewMode" }, + { id: "compact-1", type: "contextCompaction" }, + ] as const; + + for (const item of completedItems) { + await dispatchProviderNotification( + { + method: "item/completed", + params: { + completedAtMs: 2, + item, + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ); + } + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "tool.call.updated", + payload: expect.objectContaining({ + structuredOutput: { + action: { queries: ["OpenAI app-server 0.147"], query: null, type: "search" }, + query: "OpenAI app-server 0.147", + results: [{ title: "Protocol release", url: "https://example.test/release" }], + }, + toolCallId: "web-1", + }), + }), + ); + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "tool.call.updated", + payload: expect.objectContaining({ + rawOutput: "/tmp/image.png", + toolCallId: "image-view-1", + }), + }), + ); + expect(events().filter((event) => event.kind === "review.updated")).toMatchObject([ + { payload: { mode: "entered", review: "Review changes", status: "completed" } }, + { payload: { mode: "exited", review: "Review complete", status: "completed" } }, + ]); + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "context.compacted", + payload: { itemId: "compact-1", status: "completed" }, + }), + ); + + const eventCount = events().length; + for (const item of [ + { clientId: null, content: [], id: "user-echo-1", type: "userMessage" }, + { + fragments: [{ hookRunId: "hook-1", text: "provider echo" }], + id: "hook-echo-1", + type: "hookPrompt", + }, + ] as const) { + await dispatchProviderNotification( + { + method: "item/completed", + params: { completedAtMs: 3, item, threadId: "thread-1", turnId: "turn-1" }, + }, + bridge, + context, + ); + } + expect(events()).toHaveLength(eventCount); + await logger.destroy(); + }); + + test.each(["started", "interacted", "interrupted", "completed"] as const)( + "publishes official %s sub-agent activity from completion-only delivery", + async (activityKind) => { + const { bridge, context, events, logger } = createHarness(); + const item = { + agentPath: "/root/worker", + agentThreadId: "agent-1", + id: `activity-${activityKind}`, + kind: activityKind, + type: "subAgentActivity", + } as const; + + await dispatchProviderNotification( + { + method: "item/completed", + params: { completedAtMs: 2, item, threadId: "thread-1", turnId: "turn-1" }, + }, + bridge, + context, + ); + + expect(events()).toEqual([ + { + delivery: "lossless", + kind: "agent.task.updated", + payload: { + ...(activityKind === "started" + ? { active: true, status: "running" } + : activityKind === "interacted" + ? {} + : { + active: false, + status: activityKind === "interrupted" ? "cancelled" : "completed", + }), + activityKind, + agentId: "agent-1", + agentPath: "/root/worker", + taskId: "agent-1", + title: `Sub-agent ${activityKind}`, + }, + sourceEventId: `openai.item.completed:turn-1:activity-${activityKind}:0`, + }, + ]); + await logger.destroy(); + }, + ); + + test("publishes sub-agent activity from a terminal turn snapshot", async () => { + const { bridge, context, events, logger } = createHarness(); + + await dispatchProviderNotification( + { + method: "turn/completed", + params: { + threadId: "thread-1", + turn: { + id: "turn-snapshot", + items: [ + { + agentPath: "/root/replayed-worker", + agentThreadId: "agent-replayed", + id: "activity-replayed", + kind: "interacted", + type: "subAgentActivity", + }, + ], + status: "completed", + }, + }, + }, + bridge, + context, + ); + + expect(events()).toContainEqual({ + delivery: "lossless", + kind: "agent.task.updated", + payload: { + activityKind: "interacted", + agentId: "agent-replayed", + agentPath: "/root/replayed-worker", + taskId: "agent-replayed", + title: "Sub-agent interacted", + }, + sourceEventId: "openai.item.completed:turn-snapshot:activity-replayed:0", + }); + await logger.destroy(); + }); + + test.each([ + ["commandExecution", "inProgress"], + ["fileChange", "inProgress"], + ["mcpToolCall", "inProgress"], + ["dynamicToolCall", "inProgress"], + ["collabAgentToolCall", "inProgress"], + ["imageGeneration", "in_progress"], + ] as const)( + "fails closed on a schema-valid nonterminal %s completion before mutating state", + async (type, runningStatus) => { + const { bridge, context, events, logger } = createHarness(); + const id = `nonterminal-${type}`; + + await expect( + dispatchProviderNotification( + { + method: "item/completed", + params: { + completedAtMs: 2, + item: createOfficialStatusToolItem(type, runningStatus, id), + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ), + ).rejects.toThrow(`OpenAI ${type} completed with non-terminal status ${runningStatus}`); + + // A valid terminal retry for the same item must still publish, proving rejection did not + // poison the completion dedupe or tool state. + await dispatchProviderNotification( + { + method: "item/completed", + params: { + completedAtMs: 3, + item: createOfficialStatusToolItem(type, "failed", id), + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ); + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "tool.call.updated", + payload: expect.objectContaining({ status: "failed", toolCallId: id }), + }), + ); + await logger.destroy(); + }, + ); + + test("rejects completed images and diagnoses provider-declared image failure", async () => { + const { bridge, context, events, logger } = createHarness(); + + await expect( + dispatchProviderNotification( + { + method: "item/completed", + params: { + completedAtMs: 2, + item: { + id: "image-invalid", + result: "not base64", + revisedPrompt: null, + status: "completed", + type: "imageGeneration", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ), + ).rejects.toThrow("without a supported durable image transport"); + + await dispatchProviderNotification( + { + method: "item/completed", + params: { + completedAtMs: 3, + item: { + failure: { + limitId: "image_generation", + resetsAt: null, + type: "usageLimitExceeded", + }, + id: "image-failed", + result: "", + revisedPrompt: "Failed prompt", + savedPath: "/untrusted/failed.png", + status: "failed", + type: "imageGeneration", + }, + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ); + + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "diagnostic.reported", + payload: expect.objectContaining({ code: "openai.image_generation.failed" }), + visibility: "owner_debug", + }), + ); + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "tool.call.updated", + payload: expect.objectContaining({ + status: "failed", + structuredOutput: { + failure: { + limitId: "image_generation", + resetsAt: null, + type: "usageLimitExceeded", + }, + }, + toolCallId: "image-failed", + }), + }), + ); + expect(JSON.stringify(events())).not.toContain("/untrusted/failed.png"); + await logger.destroy(); + }); + test.each(providerFixtureNames)("apps provider-native fixture %s", async (name) => { const fixture = readProviderFixtureCase( `./fixtures/providers/openai-app-server/cases/${name}.json`, diff --git a/tests/openai-app-server-request-handler.test.ts b/tests/openai-app-server-request-handler.test.ts index ff6587b..70b5b28 100644 --- a/tests/openai-app-server-request-handler.test.ts +++ b/tests/openai-app-server-request-handler.test.ts @@ -2,13 +2,15 @@ import { expect, test } from "bun:test"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { PermissionEventDeliveryError } from "../src/core/driver-permission-broker"; +import type { DriverPermissionRequest } from "../src/host-ports"; import { createBufferedSinkLogger } from "../src/observability"; import { OpenAiAppServerRequestHandler } from "../src/runtimes/openai/app-server-request-handler"; import { driverStartInput } from "./driver-boot-payload-fixture"; test("OpenAI server request callbacks handle, reject, and cancel explicitly", async () => { const permissionStarted = Promise.withResolvers(); - let permissionSignal: AbortSignal | null = null; + const permissionInputs: DriverPermissionRequest[] = []; + const permissionSignals: AbortSignal[] = []; const responses: Array<{ id: string | number; result: unknown }> = []; const rejections: Array<{ id: string | number; message: string }> = []; const errors: Error[] = []; @@ -18,14 +20,26 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as sink: async () => {}, }); const context = createAgentDriverContext({ - eventSink: { pushEvents: async () => ({ accepted: [] }) }, + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, logger, payload: driverStartInput, permission: { - request: async (_input, signal) => { + request: async (input, signal) => { const requestSignal = signal ?? new AbortController().signal; - permissionSignal = requestSignal; - permissionStarted.resolve(); + permissionInputs.push(input); + permissionSignals.push(requestSignal); + if (permissionInputs.length === 3) { + permissionStarted.resolve(); + } + if (input.toolKind === "item/permissions/requestApproval") { + return "allow_once"; + } + if (input.toolKind === "item/commandExecution/requestApproval") { + return "allow_once"; + } return await new Promise<"allow_once">((_resolve, reject) => { requestSignal.addEventListener("abort", () => reject(requestSignal.reason), { once: true, @@ -40,27 +54,88 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as errors.push(error); }, isStopped: () => false, + mapToolCallId: (toolCallId) => toolCallId, respond: (id, result) => responses.push({ id, result }), respondError: (id, message) => rejections.push({ id, message }), }); try { - handler.dispatch("currentTime/read", 1, {}); + handler.dispatch("currentTime/read", 1, { threadId: "thread-1" }); handler.dispatch("attestation/generate", 2, {}); + handler.dispatch("applyPatchApproval", 6, {}); + handler.dispatch("execCommandApproval", 7, {}); handler.dispatch("item/commandExecution/requestApproval", 3, { - command: "pwd", + additionalPermissions: { + fileSystem: { write: ["/secrets"] }, + network: { enabled: true }, + }, + command: "npm test", + cwd: "/workspace", + availableDecisions: [ + "acceptForSession", + { + acceptWithExecpolicyAmendment: { + execpolicy_amendment: ["npm", "test"], + }, + }, + "cancel", + ], itemId: "tool-1", }); + handler.dispatch("item/fileChange/requestApproval", 4, { + grantRoot: "/workspace", + itemId: "tool-2", + reason: "Apply changes", + }); + handler.dispatch("item/permissions/requestApproval", 5, { + cwd: "/workspace", + environmentId: "sandbox-1", + permissions: { fileSystem: { read: ["/workspace"] }, network: null }, + reason: "Install dependencies", + }); await permissionStarted.promise; await handler.abortAll(new Error("turn cancelled")); - expect(responses).toHaveLength(1); - expect(responses[0]).toMatchObject({ id: 1, result: { currentTimeAt: expect.any(Number) } }); + expect(responses).toEqual( + expect.arrayContaining([ + { id: 1, result: { currentTimeAt: expect.any(Number) } }, + { id: 3, result: { decision: "cancel" } }, + { + id: 5, + result: { + permissions: { fileSystem: { read: ["/workspace"] } }, + scope: "turn", + }, + }, + ]), + ); expect(rejections).toEqual([ { id: 2, message: "Unsupported OpenAi app-server request: attestation/generate." }, + { id: 6, message: "Unsupported OpenAi app-server request: applyPatchApproval." }, + { id: 7, message: "Unsupported OpenAi app-server request: execCommandApproval." }, + ]); + expect(permissionSignals.filter((signal) => signal.aborted)).toHaveLength(1); + expect(permissionInputs.map(({ rawInput }) => JSON.parse(rawInput ?? "null"))).toEqual([ + expect.objectContaining({ + additionalPermissions: { + fileSystem: { write: ["/secrets"] }, + network: { enabled: true }, + }, + command: "npm test", + cwd: "/workspace", + }), + expect.objectContaining({ grantRoot: "/workspace", reason: "Apply changes" }), + expect.objectContaining({ + cwd: "/workspace", + environmentId: "sandbox-1", + permissions: { fileSystem: { read: ["/workspace"] }, network: null }, + }), + ]); + expect(permissionInputs.map(({ title }) => title)).toEqual([ + "Approve command execution", + "Approve file changes", + "Approve runtime permissions", ]); - expect((permissionSignal as AbortSignal | null)?.aborted).toBe(true); - expect(responses.some((response) => response.id === 3)).toBe(false); expect(rejections.some((response) => response.id === 3)).toBe(false); expect(errors).toEqual([]); } finally { @@ -69,6 +144,51 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as } }); +test("OpenAI writeStdin approval uses terminal-input semantics", async () => { + const permission = Promise.withResolvers(); + const response = Promise.withResolvers(); + const logger = createBufferedSinkLogger({ + level: "error", + service: "openai-request-handler-test", + sink: async () => {}, + }); + const context = createAgentDriverContext({ + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, + logger, + payload: driverStartInput, + permission: { + request: async (input) => { + permission.resolve(input); + return "allow_once"; + }, + }, + }); + const handler = new OpenAiAppServerRequestHandler({ + context, + handleError: async () => {}, + isStopped: () => false, + mapToolCallId: (toolCallId) => toolCallId, + respond: (_id, result) => response.resolve(result), + respondError: (_id, message) => response.reject(new Error(message)), + }); + + handler.dispatch("item/commandExecution/requestApproval", 1, { + approvalId: "approval-1", + itemId: "command-1", + kind: "writeStdin", + }); + + await expect(response.promise).resolves.toEqual({ decision: "accept" }); + expect(await permission.promise).toMatchObject({ + title: "Approve terminal input", + toolCallId: "command-1", + }); + await logger.destroy(); +}); + test("OpenAI server request cancellation propagates permission delivery failures", async () => { const permissionStarted = Promise.withResolvers(); const deliveryGate = Promise.withResolvers(); @@ -84,7 +204,10 @@ test("OpenAI server request cancellation propagates permission delivery failures sink: async () => {}, }); const context = createAgentDriverContext({ - eventSink: { pushEvents: async () => ({ accepted: [] }) }, + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, logger, payload: driverStartInput, permission: { @@ -109,6 +232,7 @@ test("OpenAI server request cancellation propagates permission delivery failures handledErrors.push(error); }, isStopped: () => false, + mapToolCallId: (toolCallId) => toolCallId, respond: () => {}, respondError: () => {}, }); diff --git a/tests/openai-app-server-startup.test.ts b/tests/openai-app-server-startup.test.ts index 8283115..f472571 100644 --- a/tests/openai-app-server-startup.test.ts +++ b/tests/openai-app-server-startup.test.ts @@ -7,26 +7,54 @@ import type { AgentDriverPermissionPort } from "../src/host-ports"; import { createBufferedSinkLogger } from "../src/observability"; import type { DriverPermissionPolicy } from "../src/protocol/boot"; import type { DriverEventInput } from "../src/protocol/events"; +import { isDriverId } from "../src/protocol/id"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; +import type { AgentDriverBackend } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { AgentDriverKernelCore } from "../src/core/agent-driver-kernel"; import { ACTIVE_TURN_CANCEL_GRACE_MS } from "../src/core/driver-command-dispatcher"; +import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; import { OpenAiAppServerClient } from "../src/runtimes/openai/app-server-client"; import { OpenAiAppServerDriverBackend } from "../src/runtimes/openai/app-server-driver-backend"; +import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; +import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; +import { createCmaMemoryStore } from "../src/stores/memory"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; import { settlePromiseWithTimeout } from "../src/utils/async"; const originalExecutable = process.env["MOSOO_OPENAI_RUNTIME_EXECUTABLE"]; const temporaryDirectories: string[] = []; +const initializeResultJson = JSON.stringify({ + codexHome: "/tmp/openai-home", + platformFamily: "unix", + platformOs: "linux", + userAgent: "test-app-server/0.150.1", +}); + +function eventPayloadStatus(event: DriverEventInput): unknown { + const { payload } = event; + + return typeof payload === "object" && payload !== null && !Array.isArray(payload) + ? (payload as Record)["status"] + : undefined; +} interface CancellationHarnessOptions { readonly backgroundTerminalCleanFailure?: "error" | "timeout"; + readonly duplicateRequestPhase?: "active_turn" | "turn_start"; readonly emitInterruptedTurnOnStart?: boolean; + readonly emitToolCompletionOnTurnStart?: boolean; readonly failCancellationRequest?: boolean; readonly failInitialThreadStart?: boolean; readonly holdCancellationRequest?: boolean; readonly holdRunCancellation?: boolean; + readonly holdTurnStartAfterItems?: boolean; + readonly holdToolCompletion?: boolean; readonly restartResumeError?: string; + readonly threadId?: string; + readonly turnId?: string; + readonly turnStartErrorMessage?: string; + readonly useCma?: boolean; } afterEach(async () => { @@ -96,6 +124,117 @@ const sendInterrupted = (turnId) => process.stdout.write(JSON.stringify({ }, }, }) + "\\n"); +const sendToolLifecycle = (turnId, finishTurn, completeItem = true) => { + const item = { + aggregatedOutput: "done", + command: "printf done", + commandActions: [], + cwd: ${JSON.stringify(directory)}, + durationMs: 1, + exitCode: 0, + id: "tool-1", + pluginId: null, + processId: null, + scriptPath: null, + source: "agent", + status: "completed", + type: "commandExecution", + }; + process.stdout.write(JSON.stringify({ + method: "item/started", + params: { + item: { ...item, aggregatedOutput: null, durationMs: null, exitCode: null, status: "inProgress" }, + startedAtMs: 1, + threadId: "fresh-thread", + turnId, + }, + }) + "\\n"); + if (completeItem) { + process.stdout.write(JSON.stringify({ + method: "item/completed", + params: { completedAtMs: 2, item, threadId: "fresh-thread", turnId }, + }) + "\\n"); + } + if (finishTurn) { + process.stdout.write(JSON.stringify({ + method: "turn/completed", + params: { + threadId: "fresh-thread", + turn: { + completedAt: Date.now(), + durationMs: 1, + error: null, + id: turnId, + items: [item], + itemsView: "full", + startedAt: null, + status: "completed", + }, + }, + }) + "\\n"); + } +}; +const duplicateRequestPhase = ${JSON.stringify(cancellationOptions.duplicateRequestPhase ?? null)}; +const sendDuplicateRequest = (turnId) => { + const duplicate = JSON.stringify({ + id: "x".repeat(1_100_000), + method: "item/commandExecution/requestApproval", + params: { + environmentId: null, + itemId: "item-duplicate", + startedAtMs: 1, + threadId: "fresh-thread", + turnId, + }, + }) + "\\n"; + process.stdout.write(duplicate); + process.stdout.write(duplicate); +}; +const initializeResult = ${initializeResultJson}; +const thread = { + id: ${JSON.stringify(cancellationOptions.threadId ?? "fresh-thread")}, + extra: null, + sessionId: "fresh-thread", + forkedFromId: null, + parentThreadId: null, + preview: "", + projectId: null, + ephemeral: false, + section: null, + sectionEnteredAt: null, + historyMode: "legacy", + modelProvider: "openai", + createdAt: 0, + updatedAt: 0, + recencyAt: null, + status: { type: "idle" }, + path: null, + cwd: ${JSON.stringify(directory)}, + cliVersion: "0.150.1", + source: "appServer", + canAcceptDirectInput: true, + threadSource: null, + agentNickname: null, + agentRole: null, + gitInfo: null, + name: null, + turns: [], +}; +const threadStartResult = { + thread, + model: "test-model", + modelProvider: "openai", + serviceTier: null, + cwd: ${JSON.stringify(directory)}, + runtimeWorkspaceRoots: [${JSON.stringify(directory)}], + instructionSources: [], + approvalPolicy: "never", + approvalsReviewer: "user", + sandbox: { type: "dangerFullAccess" }, + activePermissionProfile: null, + reasoningEffort: null, + multiAgentMode: "explicitRequestOnly", +}; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { buffer += chunk; @@ -105,25 +244,33 @@ process.stdin.on("data", (chunk) => { buffer = buffer.slice(newline + 1); appendFileSync(${JSON.stringify(requestLog)}, JSON.stringify(request) + "\\n"); const turnNumber = request.method === "turn/start" ? ++turnStartCount : 0; - const turnId = launchNumber === 1 + const configuredTurnId = ${JSON.stringify(cancellationOptions.turnId ?? null)}; + const turnId = configuredTurnId ?? (launchNumber === 1 ? "turn-" + turnNumber - : "turn-" + launchNumber + "-" + turnNumber; + : "turn-" + launchNumber + "-" + turnNumber); + const turnStartErrorMessage = ${JSON.stringify(cancellationOptions.turnStartErrorMessage ?? null)}; const configuredResumeError = ${JSON.stringify(resumeErrorMessage)}; const restartResumeError = ${JSON.stringify(cancellationOptions.restartResumeError ?? null)}; const resumeError = launchNumber > 1 && restartResumeError !== null ? restartResumeError - : configuredResumeError.startsWith("no rollout found for thread id ") + : request.method === "thread/resume" && + configuredResumeError.startsWith("no rollout found for thread id ") ? "no rollout found for thread id " + request.params.threadId : configuredResumeError; - const terminalTurn = launchNumber > 1 || turnNumber > 1; + const terminalTurn = + (launchNumber > 1 || turnNumber > 1) && + !${JSON.stringify(cancellationOptions.emitToolCompletionOnTurnStart ?? false)}; const holdTurnStart = - ${JSON.stringify(holdFirstTurnStartResponse)} && + (${JSON.stringify(holdFirstTurnStartResponse)} || + ${JSON.stringify(cancellationOptions.holdTurnStartAfterItems ?? false)}) && launchNumber === 1 && request.method === "turn/start" && turnNumber === 1; const response = - request.method === "thread/backgroundTerminals/clean" && + request.method === "initialized" + ? null + : request.method === "thread/backgroundTerminals/clean" && ${JSON.stringify(cancellationOptions.backgroundTerminalCleanFailure ?? null)} === "error" ? { id: request.id, error: { code: -32600, message: "background clean failed" } } : request.method === "thread/start" && @@ -133,7 +280,9 @@ process.stdin.on("data", (chunk) => { : request.method === "thread/resume" ? { id: request.id, error: { code: -32600, message: resumeError } } : request.method === "thread/start" - ? { id: request.id, result: { thread: { id: "fresh-thread" } } } + ? { id: request.id, result: threadStartResult } + : request.method === "turn/start" && turnStartErrorMessage !== null + ? { id: request.id, error: { code: -32000, message: turnStartErrorMessage } } : request.method === "turn/start" ? { id: request.id, result: { turn: { completedAt: terminalTurn ? Date.now() : null, @@ -145,18 +294,33 @@ process.stdin.on("data", (chunk) => { startedAt: null, status: terminalTurn ? "completed" : "inProgress", } } } - : { id: request.id, result: {} }; + : request.method === "initialize" + ? { id: request.id, result: initializeResult } + : { id: request.id, result: {} }; const sendApproval = () => process.stdout.write(JSON.stringify({ id: 91, method: "item/commandExecution/requestApproval", - params: { itemId: "item-1", threadId: "fresh-thread", turnId: "turn-1" }, + params: { + environmentId: null, + itemId: "item-1", + startedAtMs: 1, + threadId: "fresh-thread", + turnId: "turn-1", + }, }) + "\\n"); const sendResponse = () => { if ( request.method === "thread/backgroundTerminals/clean" && ${JSON.stringify(cancellationOptions.backgroundTerminalCleanFailure ?? null)} === "timeout" ) return; - process.stdout.write(JSON.stringify(response) + "\\n"); + if (request.method === "turn/start" && duplicateRequestPhase === "turn_start") { + sendDuplicateRequest(turnId); + return; + } + if (response !== null) process.stdout.write(JSON.stringify(response) + "\\n"); + if (request.method === "turn/start" && duplicateRequestPhase === "active_turn") { + setTimeout(() => sendDuplicateRequest(turnId), 5); + } if (${JSON.stringify(emitApproval)} && request.method === "turn/start" && !holdTurnStart) { sendApproval(); } @@ -166,10 +330,31 @@ process.stdin.on("data", (chunk) => { ) { setTimeout(() => sendInterrupted(turnId), 5); } + if ( + request.method === "turn/start" && + ${JSON.stringify(cancellationOptions.emitToolCompletionOnTurnStart ?? false)} + ) { + setTimeout( + () => + sendToolLifecycle( + turnId, + launchNumber > 1 || + turnNumber > 1 || + !${JSON.stringify(cancellationOptions.holdTurnStartAfterItems ?? false)}, + ), + 5, + ); + } }; if (holdTurnStart) { if (${JSON.stringify(emitApproval)}) sendApproval(); writeFileSync(${JSON.stringify(turnStartHeldMarker)}, ""); + if ( + request.method === "turn/start" && + ${JSON.stringify(cancellationOptions.emitToolCompletionOnTurnStart ?? false)} + ) { + setTimeout(() => sendToolLifecycle(turnId, false, false), 5); + } const gate = setInterval(() => { if (existsSync(${JSON.stringify(turnStartReleaseMarker)})) { clearInterval(gate); @@ -210,11 +395,20 @@ process.stdin.on("data", (chunk) => { }, }); const events: DriverEventInput[] = []; + const cmaEventTypes: string[] = []; + const cmaStore = + cancellationOptions.duplicateRequestPhase === undefined && cancellationOptions.useCma !== true + ? null + : createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); + let activeRunId = null as (typeof DRIVER_TEST_IDS)["runId"] | null; let cancellationRequestFailures = 0; const cancellationRequestEntered = Promise.withResolvers(); const cancellationRequestGate = Promise.withResolvers(); const runCancellationEntered = Promise.withResolvers(); const runCancellationGate = Promise.withResolvers(); + const toolCompletionEntered = Promise.withResolvers(); + const toolCompletionGate = Promise.withResolvers(); + let toolCompletionHolds = 0; const turnTimingEntered = Promise.withResolvers(); const turnTimingGate = Promise.withResolvers(); const logger = createBufferedSinkLogger({ @@ -225,6 +419,7 @@ process.stdin.on("data", (chunk) => { const context = createAgentDriverContext({ eventSink: { commandUpdate: async () => {}, + currentRunId: () => activeRunId, pushEvents: async (input) => { if ( holdTurnTiming && @@ -252,7 +447,41 @@ process.stdin.on("data", (chunk) => { runCancellationEntered.resolve(); await runCancellationGate.promise; } + if ( + cancellationOptions.holdToolCompletion === true && + input.events.some( + (event) => + event.kind === "tool.call.updated" && eventPayloadStatus(event) === "completed", + ) && + toolCompletionHolds++ === 0 + ) { + toolCompletionEntered.resolve(); + await toolCompletionGate.promise; + } + if (cmaStore !== null) { + for (const event of input.events) { + for (const envelope of toDriverEventEnvelopes(driverBootPayload, event, activeRunId)) { + const records = await cmaStore.appendDriverEvent( + DRIVER_TEST_IDS.sessionId, + envelope.event, + ); + cmaEventTypes.push(...records.map((record) => record.event.type)); + } + } + } events.push(...input.events); + for (const event of input.events) { + if (event.kind === "run.started") { + activeRunId = (event.runId as (typeof DRIVER_TEST_IDS)["runId"] | undefined) ?? null; + } + if ( + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed" + ) { + activeRunId = null; + } + } return { accepted: input.events.map((event, index) => ({ seq: index + 1, @@ -266,10 +495,28 @@ process.stdin.on("data", (chunk) => { permission: { request: requestPermission }, ports: { skill: { materialize: async () => [] } }, }); + const backend = new OpenAiAppServerDriverBackend(payload); + const trackedBackend: AgentDriverBackend = { + runtime: backend.runtime, + cancelActiveTurn: (backendContext, reason) => backend.cancelActiveTurn(backendContext, reason), + handleInput: async (backendContext, input, runId) => { + activeRunId = runId; + try { + await backend.handleInput(backendContext, input, runId); + } finally { + if (activeRunId === runId) { + activeRunId = null; + } + } + }, + start: (backendContext, signal) => backend.start(backendContext, signal), + stop: (backendContext, reason, signal) => backend.stop(backendContext, reason, signal), + }; return { - backend: new OpenAiAppServerDriverBackend(payload), + backend: trackedBackend, cancellationRequestEntered: cancellationRequestEntered.promise, + cmaEventTypes, context, events, logger, @@ -277,12 +524,14 @@ process.stdin.on("data", (chunk) => { processLog, releaseCancellationRequest: () => cancellationRequestGate.resolve(), releaseRunCancellation: () => runCancellationGate.resolve(), + releaseToolCompletion: () => toolCompletionGate.resolve(), releaseTurnTiming: () => turnTimingGate.resolve(), releaseTurnStartResponse: () => Bun.write(turnStartReleaseMarker, ""), requestLog, runCancellationEntered: runCancellationEntered.promise, turnTimingEntered: turnTimingEntered.promise, turnStartHeldMarker, + toolCompletionEntered: toolCompletionEntered.promise, }; } @@ -291,7 +540,18 @@ function createCancellationHarness(options: CancellationHarnessOptions) { "no rollout found for thread id stale-thread", [], false, - async () => "allow_once", + async (_input, signal) => { + if (options.duplicateRequestPhase !== undefined) { + await new Promise((resolve) => { + if (signal?.aborted) { + resolve(); + return; + } + signal?.addEventListener("abort", () => resolve(), { once: true }); + }); + } + return "allow_once"; + }, false, "full_access", false, @@ -328,7 +588,13 @@ describe("OpenAI app-server startup", () => { (line) => JSON.parse(line) as { method: string; - params?: { approvalPolicy?: string }; + params?: { + approvalPolicy?: string; + cwd?: string; + input?: unknown; + model?: string; + threadId?: string; + }; }, ); expect( @@ -338,6 +604,13 @@ describe("OpenAI app-server startup", () => { ) .map((request) => request.params?.approvalPolicy), ).toEqual(["untrusted", "untrusted", "untrusted"]); + expect(requests.find((request) => request.method === "turn/start")?.params).toEqual({ + approvalPolicy: "untrusted", + cwd: harness.payload.execution.session.cwd, + input: [{ text: "hello", text_elements: [], type: "text" }], + model: harness.payload.execution.model, + threadId: "fresh-thread", + }); const stop = harness.backend.stop( harness.context, @@ -540,6 +813,58 @@ describe("OpenAI app-server startup", () => { } }); + test("fails startup before retaining an oversized native resume pointer", async () => { + const harness = await createCancellationHarness({ threadId: "t".repeat(257) }); + + try { + await expect( + harness.backend.start(harness.context, new AbortController().signal), + ).rejects.toThrow("1-256 UTF-8 bytes"); + expect(harness.events).toEqual([]); + const childPid = await readFirstLaunchPid(harness.processLog); + expect(() => process.kill(childPid, 0)).toThrow(); + } finally { + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + await harness.logger.destroy(); + } + }); + + test("maps an oversized official turn identity before timing and CMA delivery", async () => { + const nativeTurnId = `turn-${"x".repeat(1_100_000)}`; + const harness = await createCancellationHarness({ + emitToolCompletionOnTurnStart: true, + turnId: nativeTurnId, + useCma: true, + }); + + try { + await harness.backend.start(harness.context, new AbortController().signal); + await harness.backend.handleInput(harness.context, { text: "hello" }, DRIVER_TEST_IDS.runId); + + const timing = harness.events.find( + (event) => + event.kind === "runtime.timing.recorded" && + event.native?.eventName === "provider.turn_start", + ); + const started = harness.events.find((event) => event.kind === "run.started"); + const publicTurnId = timing?.native?.turnId; + expect(isDriverId(publicTurnId)).toBe(true); + expect(started?.native?.turnId).toBe(publicTurnId); + expect(timing?.sourceEventId).toBe(`openai.provider.turn_start:${publicTurnId}`); + expect( + harness.events.every((event) => Buffer.byteLength(JSON.stringify(event)) < 1_048_576), + ).toBe(true); + expect(harness.cmaEventTypes).toContain("session.status_idle"); + } finally { + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + await harness.logger.destroy(); + } + }, 10_000); + test.each(["cancel", "stop"] as const)( "%s closes a turn whose start response is waiting on event delivery", async (operation) => { @@ -690,6 +1015,62 @@ describe("OpenAI app-server startup", () => { }, ); + test("closes items emitted before a pending turn start response is cancelled", async () => { + const harness = await createCancellationHarness({ + emitToolCompletionOnTurnStart: true, + holdTurnStartAfterItems: true, + }); + + try { + await harness.backend.start(harness.context, new AbortController().signal); + const input = harness.backend.handleInput( + harness.context, + { text: "first" }, + DRIVER_TEST_IDS.runId, + ); + void input.catch(() => {}); + await expect( + settlePromiseWithTimeout( + (async () => { + while ( + !harness.events.some( + (event) => + event.kind === "tool.call.updated" && eventPayloadStatus(event) === "running", + ) + ) { + await Bun.sleep(1); + } + })(), + { label: "pre-response tool start", timeoutMs: 250 }, + ), + ).resolves.toMatchObject({ status: "completed" }); + + await harness.backend.cancelActiveTurn(harness.context, "test.cancel"); + await expect(input).rejects.toThrow("test.cancel"); + expect( + harness.events + .filter((event) => event.runId === DRIVER_TEST_IDS.runId) + .map((event) => [event.kind, eventPayloadStatus(event)]), + ).toEqual([ + ["message.started", undefined], + ["item.started", undefined], + ["tool.call.updated", "running"], + ["run.started", undefined], + ["run.cancel.requested", undefined], + ["message.cancelled", undefined], + ["tool.call.updated", "cancelled"], + ["item.completed", "cancelled"], + ["run.cancelled", undefined], + ]); + } finally { + await harness.releaseTurnStartResponse(); + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + await harness.logger.destroy(); + } + }); + test("dispatcher cancellation closes a pending turn start and recovers on a new client", async () => { const harness = await createHarness( "no rollout found for thread id stale-thread", @@ -923,6 +1304,134 @@ describe("OpenAI app-server startup", () => { } }, 10_000); + test.each(["active_turn", "turn_start"] as const)( + "bounds a duplicate-request protocol failure during %s before terminal CMA delivery", + async (duplicateRequestPhase) => { + const harness = await createCancellationHarness({ duplicateRequestPhase }); + const lifecycleFailure = Promise.withResolvers(); + const lifecycleFail = spyOn(harness.context.lifecycle, "fail").mockImplementation((error) => + lifecycleFailure.resolve(error), + ); + const protocolFailureGate = Promise.withResolvers(); + const nativeFailActiveTurns = OpenAiAppServerEventBridge.prototype.failActiveTurns; + const failActiveTurns = + duplicateRequestPhase === "turn_start" + ? spyOn(OpenAiAppServerEventBridge.prototype, "failActiveTurns").mockImplementation( + async function (this: OpenAiAppServerEventBridge, context, error) { + await protocolFailureGate.promise; + return nativeFailActiveTurns.call(this, context, error); + }, + ) + : null; + + try { + await harness.backend.start(harness.context, new AbortController().signal); + const input = harness.backend.handleInput( + harness.context, + { text: "hello" }, + DRIVER_TEST_IDS.runId, + ); + + await expect(input).rejects.toThrow( + "OpenAI provider error message was omitted because it contained 1100046 UTF-8 bytes.", + ); + protocolFailureGate.resolve(); + const terminal = harness.events.find((event) => event.kind === "run.failed"); + expect(terminal).toMatchObject({ + payload: { + error: { + code: "openai.provider_failed", + details: { messageUtf8Bytes: 1_100_046 }, + message: + "OpenAI provider error message was omitted because it contained 1100046 UTF-8 bytes.", + retryable: false, + }, + recoverable: false, + }, + }); + expect(Buffer.byteLength(JSON.stringify(terminal), "utf8")).toBeLessThan(1_048_576); + expect(harness.cmaEventTypes).toContain("session.error"); + if (duplicateRequestPhase === "turn_start") { + await expect( + settlePromiseWithTimeout(lifecycleFailure.promise, { + label: "bounded turn-start protocol lifecycle failure", + timeoutMs: 1_000, + }), + ).resolves.toMatchObject({ + status: "completed", + value: { + message: + "OpenAI provider error message was omitted because it contained 1100046 UTF-8 bytes.", + }, + }); + } else { + expect(lifecycleFail).not.toHaveBeenCalled(); + } + } finally { + protocolFailureGate.resolve(); + failActiveTurns?.mockRestore(); + lifecycleFail.mockRestore(); + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + await harness.logger.destroy(); + } + }, + 10_000, + ); + + test("bounds an official turn start error before terminal CMA and dispatcher delivery", async () => { + const message = "x".repeat(1_100_000); + const harness = await createCancellationHarness({ + turnStartErrorMessage: message, + useCma: true, + }); + + try { + await harness.backend.start(harness.context, new AbortController().signal); + let failure: Error | null = null; + + try { + await harness.backend.handleInput( + harness.context, + { text: "hello" }, + DRIVER_TEST_IDS.runId, + ); + } catch (error) { + failure = error instanceof Error ? error : new Error("turn start failed"); + } + + expect(failure?.message).toBe( + "OpenAI provider error message was omitted because it contained 1100000 UTF-8 bytes.", + ); + expect( + harness.events + .filter((event) => event.runId === DRIVER_TEST_IDS.runId) + .map((event) => event.kind), + ).toEqual(["run.started", "run.failed"]); + const terminal = harness.events.find((event) => event.kind === "run.failed"); + expect(terminal).toMatchObject({ + payload: { + error: { + code: "openai.provider_failed", + details: { messageUtf8Bytes: 1_100_000 }, + message: + "OpenAI provider error message was omitted because it contained 1100000 UTF-8 bytes.", + retryable: false, + }, + recoverable: false, + }, + }); + expect(Buffer.byteLength(JSON.stringify(terminal), "utf8")).toBeLessThan(1_048_576); + expect(harness.cmaEventTypes).toContain("session.error"); + } finally { + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + await harness.logger.destroy(); + } + }, 10_000); + test("retries a retained cancellation terminal on the replacement client", async () => { const harness = await createCancellationHarness({ failCancellationRequest: true, @@ -1028,6 +1537,81 @@ describe("OpenAI app-server startup", () => { } }); + test("drains a pending item completion before cancellation and keeps the next turn clean", async () => { + const harness = await createCancellationHarness({ + emitToolCompletionOnTurnStart: true, + holdToolCompletion: true, + }); + + try { + await harness.backend.start(harness.context, new AbortController().signal); + const firstInput = harness.backend.handleInput( + harness.context, + { text: "first" }, + DRIVER_TEST_IDS.runId, + ); + void firstInput.catch(() => {}); + await harness.toolCompletionEntered; + + const cancellation = harness.backend.cancelActiveTurn(harness.context, "test.cancel"); + void cancellation.catch(() => {}); + let cancellationSettled = false; + void cancellation.finally(() => { + cancellationSettled = true; + }); + await Bun.sleep(10); + expect(cancellationSettled).toBe(false); + + await expect( + settlePromiseWithTimeout(cancellation, { + label: "cancellation after pending item delivery", + timeoutMs: ACTIVE_TURN_CANCEL_GRACE_MS, + }), + ).resolves.toMatchObject({ status: "completed" }); + expect( + harness.events.some( + (event) => event.runId === DRIVER_TEST_IDS.runId && event.kind === "run.cancelled", + ), + ).toBe(false); + + harness.releaseToolCompletion(); + await expect(firstInput).rejects.toThrow("test.cancel"); + await expect( + settlePromiseWithTimeout( + harness.backend.handleInput( + harness.context, + { text: "second" }, + DRIVER_TEST_IDS.secondRunId, + ), + { + label: "turn after pending item cancellation", + timeoutMs: ACTIVE_TURN_CANCEL_GRACE_MS, + }, + ), + ).resolves.toMatchObject({ status: "completed" }); + + const secondStarted = harness.events.filter( + (event) => event.kind === "item.started" && event.runId === DRIVER_TEST_IDS.secondRunId, + ); + expect(secondStarted).toHaveLength(1); + expect( + harness.events + .filter( + (event) => + event.runId === DRIVER_TEST_IDS.secondRunId && + ["run.cancelled", "run.completed", "run.failed"].includes(event.kind), + ) + .map((event) => event.kind), + ).toEqual(["run.completed"]); + } finally { + harness.releaseToolCompletion(); + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + await harness.logger.destroy(); + } + }); + test("retains a client when cancellation cleanup fails so shutdown can retry", async () => { const harness = await createCancellationHarness({}); const nativeKill = process.kill; @@ -1093,6 +1677,121 @@ describe("OpenAI app-server startup", () => { } }, 10_000); + test.each(["client cleanup", "terminal delivery", "late terminal delivery"] as const)( + "classifies active cancellation %s failure as an input failure", + async (failureMode) => { + const harness = await createCancellationHarness({}); + const backend = new OpenAiAppServerDriverBackend(harness.payload); + const kernel = new AgentDriverKernelCore({ + backendFactory: () => backend, + hostPorts: { skill: { materialize: async () => [] } }, + logger: harness.logger, + }); + const nativeStop = OpenAiAppServerClient.prototype.stop; + let failNextStop = false; + const stopSpy = + failureMode === "client cleanup" + ? spyOn(OpenAiAppServerClient.prototype, "stop").mockImplementation(function ( + this: OpenAiAppServerClient, + signal?: AbortSignal, + ) { + if (failNextStop) { + failNextStop = false; + return Promise.reject(new Error("test cancellation cleanup failed")); + } + return nativeStop.call(this, signal); + }) + : null; + const terminalEntered = Promise.withResolvers(); + const terminalGate = Promise.withResolvers(); + const terminalSpy = + failureMode !== "client cleanup" + ? spyOn(DriverEventPublisher.prototype, "pushTerminal").mockImplementation(async () => { + terminalEntered.resolve(); + if (failureMode === "late terminal delivery") { + await terminalGate.promise; + } + throw new Error("test cancellation event delivery failed"); + }) + : null; + const events = kernel.events()[Symbol.asyncIterator](); + const terminalKinds: string[] = []; + + try { + await kernel.start(harness.payload); + const input = kernel.dispatch({ + commandId: "input-before-cleanup-failure", + input: { text: "hello" }, + kind: "input.start", + requestId: "request-before-cleanup-failure", + runId: DRIVER_TEST_IDS.runId, + }); + void input.catch(() => {}); + await expect( + settlePromiseWithTimeout( + (async () => { + for (;;) { + const event = (await events.next()).value; + if (event === undefined) { + throw new Error("Kernel event stream ended before run.started."); + } + if (["run.cancelled", "run.completed", "run.failed"].includes(event.kind)) { + terminalKinds.push(event.kind); + } + if (event.kind === "run.started") { + return; + } + } + })(), + { label: "OpenAI turn before cancellation cleanup failure", timeoutMs: 1_000 }, + ), + ).resolves.toMatchObject({ status: "completed" }); + + failNextStop = failureMode === "client cleanup"; + const cancellation = kernel.cancel("test.cancel"); + void cancellation.catch(() => {}); + if (failureMode === "late terminal delivery") { + await terminalEntered.promise; + await Bun.sleep(300); + terminalGate.resolve(); + } + await expect(input).rejects.toThrow("test cancellation"); + if (failureMode === "late terminal delivery") { + await expect(cancellation).resolves.toBeUndefined(); + } else { + await expect(cancellation).rejects.toThrow("test cancellation"); + } + await expect( + settlePromiseWithTimeout( + (async () => { + for (;;) { + const event = (await events.next()).value; + if (event === undefined) { + throw new Error("Kernel event stream ended before run.failed."); + } + if (["run.cancelled", "run.completed", "run.failed"].includes(event.kind)) { + terminalKinds.push(event.kind); + } + if (event.kind === "run.failed") { + return; + } + } + })(), + { label: "OpenAI cancellation cleanup run failure", timeoutMs: 1_000 }, + ), + ).resolves.toMatchObject({ status: "completed" }); + expect(terminalKinds).toEqual(["run.failed"]); + } finally { + failNextStop = false; + await kernel.stop("test complete").catch(() => {}); + stopSpy?.mockRestore(); + terminalSpy?.mockRestore(); + await harness.logger.destroy(); + } + }, + 10_000, + ); + test("retains a client whose process stop fails so shutdown can retry", async () => { const harness = await createCancellationHarness({}); let stopped = false; diff --git a/tests/openai-app-server-turn-start.test.ts b/tests/openai-app-server-turn-start.test.ts deleted file mode 100644 index 71f98ed..0000000 --- a/tests/openai-app-server-turn-start.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { createTurnParams } from "../src/runtimes/openai/app-server-driver-backend"; - -describe("OpenAI app-server turn start params", () => { - test("carries the resolved approval policy with every user turn", () => { - expect( - createTurnParams({ - approvalPolicy: "never", - cwd: "/workspace", - model: "gpt-5.4", - text: "Run pwd", - threadId: "thread-1", - }), - ).toEqual({ - approvalPolicy: "never", - cwd: "/workspace", - input: [ - { - text: "Run pwd", - text_elements: [], - type: "text", - }, - ], - model: "gpt-5.4", - threadId: "thread-1", - }); - }); -}); diff --git a/tests/openai-contract-adapter-interactions.test.ts b/tests/openai-contract-adapter-interactions.test.ts deleted file mode 100644 index ac4fe57..0000000 --- a/tests/openai-contract-adapter-interactions.test.ts +++ /dev/null @@ -1,662 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - ProtocolAdmissionLimits, - Run, - SessionSnapshot, -} from "../src/contract"; -import { - OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION, - OpenAiContractAdapter, - type OpenAiAuthorityUpdate, -} from "../src/runtimes/openai/contract-adapter"; -import type { ContractPreviewUpdate } from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const COMMAND_ID = protocolId(3); -const THREAD_ID = "thread-1"; -const TURN_ID = "turn-1"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - [OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION]: {}, - "interaction.input": {}, - "interaction.permission": {}, - "interaction.tool": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - "openai.app-server/thread-item": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - options: { - admissionLimits?: ProtocolAdmissionLimits; - holdAuthority?: boolean; - interactionTimeoutMs?: number; - maxPendingServerRequestBytes?: number; - previewCheckpointBytes?: number; - previewReplaceIntervalMs?: number; - } = {}, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - let rejectNextAuthorityAfterCommit = false; - const authorityEntered = Promise.withResolvers(); - const authorityGate = Promise.withResolvers(); - const authority: OpenAiAuthorityUpdate[] = []; - const committedMutationIds = new Set(); - const previews: ContractPreviewUpdate[] = []; - const commit = ( - cause: CommittedMutation["cause"], - operations: AuthorityOperation[], - mutationId = protocolId(1_000 + snapshot.revision + 1), - ): void => { - if (committedMutationIds.has(mutationId)) { - return; - } - - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId, - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - committedMutationIds.add(mutationId); - }; - const adapter = new OpenAiContractAdapter({ - admissionLimits: options.admissionLimits, - authority: async (update) => { - authority.push(update); - if (options.holdAuthority === true) { - authorityEntered.resolve(); - await authorityGate.promise; - } - commit(update.cause, [...update.operations] as AuthorityOperation[], update.mutationId); - if (rejectNextAuthorityAfterCommit) { - rejectNextAuthorityAfterCommit = false; - throw new AuthorityOutcomeUnknownError("authority result lost"); - } - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs: options.interactionTimeoutMs, - maxPendingServerRequestBytes: options.maxPendingServerRequestBytes, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - previewCheckpointBytes: options.previewCheckpointBytes, - previewReplaceIntervalMs: options.previewReplaceIntervalMs, - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - authorityEntered: authorityEntered.promise, - previews, - rejectNextAuthorityAfterCommit() { - rejectNextAuthorityAfterCommit = true; - }, - releaseAuthority() { - authorityGate.resolve(); - }, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - const authoritativeResolution = - resolution?.kind === "input" && resolution.value.type === "answered" - ? { - answeredQuestionIds: Object.keys(resolution.value.answers), - type: "answered" as const, - } - : resolution?.value; - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: authoritativeResolution, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -function turnAttachment() { - return { - cause: { commandId: COMMAND_ID, type: "command" as const }, - run: activeRun("2026-07-16T08:00:00.000Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }; -} - -async function registerTurn(adapter: OpenAiContractAdapter): Promise { - await adapter.attachTurn(turnAttachment()); -} - -describe("OpenAI Contract adapter", () => { - test("checkpoints long Preview text before opening a bounded next segment", async () => { - const harness = createHarness({ - previewCheckpointBytes: 5, - previewReplaceIntervalMs: 10_000, - }); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { id: "message-1", text: "", type: "agentMessage" }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "hello", - itemId: "message-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - expect(harness.previews).toHaveLength(0); - expect(harness.snapshot().items[0]).toMatchObject({ - content: [{ text: "hello", type: "text" }], - status: "active", - }); - - await harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "!", - itemId: "message-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - expect(harness.previews[0]?.update).toMatchObject({ - fromSequence: 1, - op: "append", - segment: 1, - text: "!", - throughSequence: 1, - }); - }); - - test("preserves reasoning section boundaries and replaces MCP progress snapshots", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { content: [], id: "reasoning-1", summary: [], type: "reasoning" }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/reasoning/summaryPartAdded", { - itemId: "reasoning-1", - summaryIndex: 0, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/reasoning/summaryTextDelta", { - delta: "first", - itemId: "reasoning-1", - summaryIndex: 0, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/reasoning/summaryPartAdded", { - itemId: "reasoning-1", - summaryIndex: 1, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/reasoning/summaryTextDelta", { - delta: "second", - itemId: "reasoning-1", - summaryIndex: 1, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/started", { - item: { - arguments: {}, - error: null, - id: "mcp-1", - result: null, - server: "demo", - status: "inProgress", - tool: "work", - type: "mcpToolCall", - }, - startedAtMs: Date.parse("2026-07-16T08:00:00.200Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/mcpToolCall/progress", { - itemId: "mcp-1", - message: "one", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/mcpToolCall/progress", { - itemId: "mcp-1", - message: "two", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - expect(harness.previews.map((entry) => entry.update)).toMatchObject([ - { channel: "reasoning.text", op: "append", text: "first", throughSequence: 1 }, - { channel: "reasoning.text", op: "append", text: "\n\n", throughSequence: 2 }, - { channel: "reasoning.text", op: "append", text: "second", throughSequence: 3 }, - { channel: "tool.progress", op: "replace", text: "one", throughSequence: 1 }, - { channel: "tool.progress", op: "replace", text: "two", throughSequence: 2 }, - ]); - }); - - test("removes private citation markup from Preview and authoritative messages", async () => { - const harness = createHarness(); - const citation = "\uE200cite\uE202turn7search12\uE201"; - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { id: "message-1", text: "", type: "agentMessage" }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "before\uE200ci", - itemId: "message-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "te\uE202turn7search12\uE201after", - itemId: "message-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.300Z"), - item: { id: "message-1", text: `before${citation}after`, type: "agentMessage" }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - expect(harness.previews.map((entry) => entry.update)).toMatchObject([ - { text: "before", throughSequence: 1 }, - { text: "after", throughSequence: 2 }, - ]); - expect(harness.snapshot().items[0]).toMatchObject({ - content: [{ text: "beforeafter", type: "text" }], - status: "completed", - }); - }); - - test("projects usage and authoritative terminal, change, MCP, and plan snapshots", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { - aggregatedOutput: null, - command: "pwd", - cwd: "/workspace", - exitCode: null, - id: "command-1", - status: "inProgress", - type: "commandExecution", - }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.200Z"), - item: { - aggregatedOutput: "/workspace\n", - command: "pwd", - cwd: "/workspace", - exitCode: 0, - id: "command-1", - status: "completed", - type: "commandExecution", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/fileChange/patchUpdated", { - changes: [ - { - diff: "+hello", - kind: { type: "add" }, - path: "hello.txt", - }, - ], - itemId: "change-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.300Z"), - item: { - changes: [ - { - diff: "+hello", - kind: { type: "add" }, - path: "hello.txt", - }, - ], - id: "change-1", - status: "completed", - type: "fileChange", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/started", { - item: { - arguments: { path: "README.md" }, - error: null, - id: "mcp-1", - result: null, - server: "files", - status: "inProgress", - tool: "read", - type: "mcpToolCall", - }, - startedAtMs: Date.parse("2026-07-16T08:00:00.400Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.500Z"), - item: { - arguments: { path: "README.md" }, - error: null, - id: "mcp-1", - result: { - _meta: null, - content: [{ text: "ok", type: "text" }], - structuredContent: { bytes: 2 }, - }, - server: "files", - status: "completed", - tool: "read", - type: "mcpToolCall", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/started", { - item: { - action: null, - id: "search-1", - query: "", - results: null, - type: "webSearch", - }, - startedAtMs: Date.parse("2026-07-16T08:00:00.550Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.560Z"), - item: { - action: { query: "protocol", queries: null, type: "search" }, - id: "search-1", - query: "protocol", - results: [{ title: "Result", type: "text_result", url: "https://example.com" }], - type: "webSearch", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.570Z"), - item: { id: "native-plan", text: "Inspect the protocol", type: "plan" }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("turn/plan/updated", { - explanation: "work", - plan: [{ status: "inProgress", step: "inspect" }], - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("thread/tokenUsage/updated", { - threadId: THREAD_ID, - tokenUsage: { - last: { - cachedInputTokens: 2, - inputTokens: 10, - outputTokens: 4, - reasoningOutputTokens: 1, - totalTokens: 14, - }, - total: { - cachedInputTokens: 22, - inputTokens: 110, - outputTokens: 54, - reasoningOutputTokens: 11, - totalTokens: 164, - }, - }, - turnId: TURN_ID, - }); - - await harness.adapter.handleNotification("thread/tokenUsage/updated", { - threadId: THREAD_ID, - tokenUsage: { - last: { - cachedInputTokens: 1, - inputTokens: 3, - outputTokens: 2, - reasoningOutputTokens: 1, - totalTokens: 5, - }, - total: { - cachedInputTokens: 23, - inputTokens: 113, - outputTokens: 56, - reasoningOutputTokens: 12, - totalTokens: 169, - }, - }, - turnId: TURN_ID, - }); - - const snapshot = harness.snapshot(); - expect(snapshot.runs[0]?.usage).toEqual({ - cachedInput: 3, - input: 13, - output: 6, - reasoning: 2, - total: 19, - }); - expect(snapshot.items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - command: "pwd", - exitCode: 0, - kind: "terminal", - status: "completed", - stdout: [{ text: "/workspace\n", type: "text" }], - }), - expect.objectContaining({ - changes: [ - { - diff: { text: "+hello", type: "text" }, - operation: "create", - path: "hello.txt", - }, - ], - kind: "change", - status: "completed", - }), - expect.objectContaining({ - input: { path: "README.md" }, - kind: "tool", - name: "read", - origin: "mcp", - output: [{ text: "ok", type: "text" }], - server: "files", - status: "completed", - structuredOutput: { bytes: 2 }, - }), - expect.objectContaining({ - input: { - action: { query: "protocol", queries: null, type: "search" }, - query: "protocol", - }, - kind: "tool", - name: "web_search", - structuredOutput: [{ title: "Result", type: "text_result", url: "https://example.com" }], - }), - expect.objectContaining({ - entries: [{ id: "0", status: "in_progress", text: "inspect" }], - explanation: "work", - id: "turn-plan", - kind: "plan", - status: "active", - }), - expect.objectContaining({ - entries: [{ id: "0", status: "completed", text: "Inspect the protocol" }], - id: "native-plan", - kind: "plan", - status: "completed", - }), - ]), - ); - - await harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { - completedAt: Date.parse("2026-07-16T08:00:00.600Z") / 1_000, - error: null, - id: TURN_ID, - items: [], - itemsView: "notLoaded", - startedAt: Date.parse("2026-07-16T08:00:00.000Z") / 1_000, - status: "completed", - }, - }); - expect(harness.snapshot().runs[0]).toMatchObject({ status: "completed" }); - expect(harness.snapshot().items.find((item) => item.id === "turn-plan")).toMatchObject({ - status: "completed", - }); - }); - - test.each([ - { - error: { message: "command failed explicitly" }, - expectedError: "command failed explicitly", - expectedStatus: "failed", - label: "explicit failure", - nativeStatus: "failed", - }, - { - error: null, - expectedError: "commandExecution failed.", - expectedStatus: "failed", - label: "fallback failure", - nativeStatus: "failed", - }, - { - error: null, - expectedError: null, - expectedStatus: "cancelled", - label: "declined command", - nativeStatus: "declined", - }, - ] as const)( - "projects $label as $expectedStatus", - async ({ error, expectedError, expectedStatus, nativeStatus }) => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - item: { - aggregatedOutput: "command output", - command: "false", - error, - exitCode: nativeStatus === "failed" ? 1 : null, - id: `command-${nativeStatus}-${expectedError ?? "none"}`, - status: nativeStatus, - type: "commandExecution", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - const item = harness.snapshot().items[0]; - expect(item).toMatchObject({ kind: "terminal", status: expectedStatus }); - - if (expectedError === null) { - expect(item).not.toHaveProperty("error"); - } else { - expect(item).toMatchObject({ error: { message: expectedError, retryable: false } }); - } - }, - ); -}); diff --git a/tests/openai-contract-adapter-items.test.ts b/tests/openai-contract-adapter-items.test.ts deleted file mode 100644 index 846ef47..0000000 --- a/tests/openai-contract-adapter-items.test.ts +++ /dev/null @@ -1,672 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - itemSchema, - previewUpdateSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - ProtocolAdmissionLimits, - Run, - SessionSnapshot, -} from "../src/contract"; -import { - OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION, - OpenAiContractAdapter, - type OpenAiAuthorityUpdate, -} from "../src/runtimes/openai/contract-adapter"; -import type { ContractPreviewUpdate } from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const COMMAND_ID = protocolId(3); -const THREAD_ID = "thread-1"; -const TURN_ID = "turn-1"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - [OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION]: {}, - "interaction.input": {}, - "interaction.permission": {}, - "interaction.tool": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - "openai.app-server/thread-item": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - options: { - admissionLimits?: ProtocolAdmissionLimits; - holdAuthority?: boolean; - interactionTimeoutMs?: number; - maxPendingServerRequestBytes?: number; - previewCheckpointBytes?: number; - previewReplaceIntervalMs?: number; - } = {}, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - let rejectNextAuthorityAfterCommit = false; - const authorityEntered = Promise.withResolvers(); - const authorityGate = Promise.withResolvers(); - const authority: OpenAiAuthorityUpdate[] = []; - const committedMutationIds = new Set(); - const previews: ContractPreviewUpdate[] = []; - const commit = ( - cause: CommittedMutation["cause"], - operations: AuthorityOperation[], - mutationId = protocolId(1_000 + snapshot.revision + 1), - ): void => { - if (committedMutationIds.has(mutationId)) { - return; - } - - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId, - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - committedMutationIds.add(mutationId); - }; - const adapter = new OpenAiContractAdapter({ - admissionLimits: options.admissionLimits, - authority: async (update) => { - authority.push(update); - if (options.holdAuthority === true) { - authorityEntered.resolve(); - await authorityGate.promise; - } - commit(update.cause, [...update.operations] as AuthorityOperation[], update.mutationId); - if (rejectNextAuthorityAfterCommit) { - rejectNextAuthorityAfterCommit = false; - throw new AuthorityOutcomeUnknownError("authority result lost"); - } - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs: options.interactionTimeoutMs, - maxPendingServerRequestBytes: options.maxPendingServerRequestBytes, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - previewCheckpointBytes: options.previewCheckpointBytes, - previewReplaceIntervalMs: options.previewReplaceIntervalMs, - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - authorityEntered: authorityEntered.promise, - previews, - rejectNextAuthorityAfterCommit() { - rejectNextAuthorityAfterCommit = true; - }, - releaseAuthority() { - authorityGate.resolve(); - }, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - const authoritativeResolution = - resolution?.kind === "input" && resolution.value.type === "answered" - ? { - answeredQuestionIds: Object.keys(resolution.value.answers), - type: "answered" as const, - } - : resolution?.value; - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: authoritativeResolution, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -function turnAttachment() { - return { - cause: { commandId: COMMAND_ID, type: "command" as const }, - run: activeRun("2026-07-16T08:00:00.000Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }; -} - -async function registerTurn(adapter: OpenAiContractAdapter): Promise { - await adapter.attachTurn(turnAttachment()); -} - -function userInputRequest(autoResolutionMs: number | null = null) { - return { - autoResolutionMs, - itemId: "input-1", - questions: [ - { - id: "name", - isOther: false, - isSecret: false, - options: null, - question: "Name?", - }, - ], - threadId: THREAD_ID, - turnId: TURN_ID, - }; -} - -describe("OpenAI Contract adapter", () => { - test("binds each Run to exactly one native Turn", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - - await expect( - harness.adapter.attachTurn({ - cause: { commandId: protocolId(4), type: "command" }, - run: activeRun("2026-07-16T08:00:00.000Z"), - threadId: THREAD_ID, - turnId: "turn-2", - }), - ).rejects.toThrow("already attached"); - }); - - test("replays a terminal Turn that arrives before attachment", async () => { - const harness = createHarness(); - const terminal = { - threadId: THREAD_ID, - turn: { - completedAt: Date.parse("2026-07-16T08:00:00.200Z") / 1_000, - durationMs: 200, - error: null, - id: TURN_ID, - items: [], - itemsView: "notLoaded", - startedAt: Date.parse("2026-07-16T08:00:00.000Z") / 1_000, - status: "completed", - }, - }; - - await harness.adapter.handleNotification("turn/completed", terminal); - - expect(harness.snapshot().runs[0]).toMatchObject({ status: "active" }); - await registerTurn(harness.adapter); - expect(harness.snapshot().runs[0]).toMatchObject({ - finishReason: "success", - status: "completed", - }); - await expect(registerTurn(harness.adapter)).resolves.toBeUndefined(); - await expect( - harness.adapter.handleNotification("turn/completed", terminal), - ).resolves.toBeUndefined(); - expect(harness.snapshot().runs).toHaveLength(1); - await expect( - harness.adapter.attachTurn({ - ...turnAttachment(), - run: { - ...turnAttachment().run, - input: [{ text: "changed", type: "text" }], - }, - }), - ).rejects.toThrow("different state"); - }); - - test("replays item events before a pre-attachment terminal Turn", async () => { - const harness = createHarness(); - - await harness.adapter.handleNotification("item/started", { - item: { id: "message-1", text: "", type: "agentMessage" }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "hello", - itemId: "message-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - item: { id: "message-1", text: "hello", type: "agentMessage" }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { - id: TURN_ID, - items: [], - itemsView: "notLoaded", - status: "completed", - }, - }); - - await registerTurn(harness.adapter); - - expect(harness.snapshot()).toMatchObject({ - items: [ - { - content: [{ text: "hello", type: "text" }], - id: "message-1", - status: "completed", - }, - ], - runs: [{ id: RUN_ID, status: "completed" }], - }); - }); - - test("fails closed for a server request received before Turn attachment", async () => { - const harness = createHarness(); - - await expect( - harness.adapter.handleServerRequest("item/tool/requestUserInput", 1, userInputRequest()), - ).rejects.toThrow("before attachment"); - expect(harness.authority).toHaveLength(0); - }); - - test("bounds non-terminal Turn events waiting for attachment", async () => { - const harness = createHarness(); - - for (let index = 0; index < 1_024; index += 1) { - await harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "x", - itemId: "message-1", - threadId: THREAD_ID, - turnId: `turn-${String(index)}`, - }); - } - - await expect( - harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "x", - itemId: "message-1", - threadId: THREAD_ID, - turnId: "turn-overflow", - }), - ).rejects.toThrow("pending Turn event limit"); - harness.adapter.dispose(); - }); - - test("bounds terminal Turns waiting for attachment", async () => { - const harness = createHarness(); - const terminal = (turnId: string) => - harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { - completedAt: null, - durationMs: null, - error: null, - id: turnId, - items: [], - itemsView: "notLoaded", - startedAt: null, - status: "completed", - }, - }); - - for (let index = 0; index < 1_024; index += 1) { - await terminal(`turn-${String(index)}`); - } - - await expect(terminal("turn-overflow")).rejects.toThrow("pending terminal Turn limit"); - harness.adapter.dispose(); - }); - - test("keeps a pre-attachment terminal Turn identity stable", async () => { - const harness = createHarness(); - const terminal = { - threadId: THREAD_ID, - turn: { - completedAt: null, - durationMs: null, - error: null, - id: TURN_ID, - items: [], - itemsView: "notLoaded", - startedAt: null, - status: "completed", - }, - }; - - await harness.adapter.handleNotification("turn/completed", terminal); - await expect( - harness.adapter.handleNotification("turn/completed", structuredClone(terminal)), - ).resolves.toBeUndefined(); - await expect( - harness.adapter.handleNotification("turn/completed", { - ...terminal, - turn: { ...terminal.turn, status: "failed" }, - }), - ).rejects.toThrow("changed before attachment"); - harness.adapter.dispose(); - }); - - test("rejects changed Run state on an attachment retry", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - - await expect( - harness.adapter.attachTurn({ - cause: { commandId: protocolId(4), type: "command" }, - run: { - ...activeRun("2026-07-16T08:00:00.000Z"), - input: [{ text: "changed", type: "text" }], - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }), - ).rejects.toThrow("different state"); - expect(harness.authority).toHaveLength(1); - }); - - test("reuses the attachment mutation ID after an ambiguous Authority result", async () => { - const harness = createHarness(); - harness.rejectNextAuthorityAfterCommit(); - - await expect(harness.adapter.attachTurn(turnAttachment())).rejects.toThrow("result lost"); - const mutationId = harness.authority[0]?.mutationId; - await expect(harness.adapter.attachTurn(turnAttachment())).resolves.toBeUndefined(); - - expect(harness.authority.map((update) => update.mutationId)).toEqual([mutationId, mutationId]); - }); - - test("does not enter Authority when disposal wins an attachment race", async () => { - const harness = createHarness(); - const attachment = harness.adapter.attachTurn(turnAttachment()); - harness.adapter.dispose(); - - await expect(attachment).rejects.toThrow("disposed"); - expect(harness.authority).toHaveLength(0); - }); - - test("does not revive local state when disposal follows an entered Authority write", async () => { - const harness = createHarness({ holdAuthority: true }); - const attachment = harness.adapter.attachTurn(turnAttachment()); - await harness.authorityEntered; - harness.adapter.dispose(); - harness.releaseAuthority(); - - await expect(attachment).resolves.toBeUndefined(); - expect(harness.authority).toHaveLength(1); - await expect( - harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { id: TURN_ID, status: "completed" }, - }), - ).rejects.toThrow("disposed"); - }); - - test.each(["item", "patch", "plan", "resolved request", "terminal"] as const)( - "reuses the first %s intent after an ambiguous Authority result", - async (kind: "item" | "patch" | "plan" | "resolved request" | "terminal") => { - const harness = createHarness(); - await registerTurn(harness.adapter); - let method: string; - let params: Record; - let expectedEntity: AuthorityOperation["entity"]; - - switch (kind) { - case "item": - method = "item/completed"; - params = { - item: { id: "message-1", text: "hello", type: "agentMessage" }, - threadId: THREAD_ID, - turnId: TURN_ID, - }; - expectedEntity = "item"; - break; - case "patch": - method = "item/fileChange/patchUpdated"; - params = { - changes: [{ diff: "+hello", kind: { type: "add" }, path: "hello.txt" }], - itemId: "change-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }; - expectedEntity = "item"; - break; - case "plan": - method = "turn/plan/updated"; - params = { - explanation: "work", - plan: [{ status: "inProgress", step: "inspect" }], - threadId: THREAD_ID, - turnId: TURN_ID, - }; - expectedEntity = "item"; - break; - case "resolved request": - await harness.adapter.handleServerRequest( - "item/tool/requestUserInput", - 61, - userInputRequest(), - ); - method = "serverRequest/resolved"; - params = { requestId: 61, threadId: THREAD_ID }; - expectedEntity = "interaction"; - break; - case "terminal": - method = "turn/completed"; - params = { - threadId: THREAD_ID, - turn: { - id: TURN_ID, - items: [], - itemsView: "notLoaded", - status: "completed", - }, - }; - expectedEntity = "run"; - break; - } - - harness.rejectNextAuthorityAfterCommit(); - await expect(harness.adapter.handleNotification(method, params)).rejects.toThrow( - "result lost", - ); - const first = harness.authority.at(-1)!; - harness.advance(1_000); - await expect( - harness.adapter.handleNotification(method, structuredClone(params)), - ).resolves.toBeUndefined(); - - const retries = harness.authority.slice(-2); - expect(retries.map((update) => update.mutationId)).toEqual([ - first.mutationId, - first.mutationId, - ]); - expect(retries.map((update) => update.operations)).toEqual([ - first.operations, - first.operations, - ]); - expect(first.operations.some((operation) => operation.entity === expectedEntity)).toBe(true); - expect(JSON.stringify(first.operations)).toContain("2026-07-16T08:00:00.000Z"); - }, - ); - - test.each([ - [ - "encoded Run", - { maxBytes: 1_500, maxInlineBytes: 1_024 }, - [{ text: "x".repeat(2_048), type: "text" as const }], - "encoded byte limit", - ], - [ - "inline Blob", - { maxBytes: 10_000, maxInlineBytes: 1 }, - [{ data: "aGVsbG8=", mediaType: "text/plain", type: "inline_blob" as const }], - "inline Blob", - ], - ] as const)( - "rejects an oversized attachment %s before Authority", - async (_, limits, input, error) => { - const harness = createHarness({ admissionLimits: limits }); - - await expect( - harness.adapter.attachTurn({ - ...turnAttachment(), - run: { ...turnAttachment().run, input: [...input] }, - }), - ).rejects.toThrow(error); - expect(harness.authority).toHaveLength(0); - }, - ); - - test("projects streaming text as Preview and repairs dropped Preview with the final Item", async () => { - const harness = createHarness({ previewReplaceIntervalMs: 1_000 }); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { id: "message-1", phase: "final_answer", text: "", type: "agentMessage" }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "hel", - itemId: "message-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - harness.advance(1_100); - await harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "lo", - itemId: "message-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - expect(harness.previews.map((entry) => previewUpdateSchema.parse(entry.update))).toEqual([ - { - channel: "message.text", - fromSequence: 1, - itemId: "message-1", - op: "append", - segment: 0, - streamId: "message.text", - text: "hel", - throughSequence: 1, - }, - { - channel: "message.text", - itemId: "message-1", - op: "replace", - segment: 0, - streamId: "message.text", - text: "hello", - throughSequence: 2, - }, - ]); - - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:01.300Z"), - item: { - id: "message-1", - phase: "final_answer", - text: "hello", - type: "agentMessage", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { - completedAt: Date.parse("2026-07-16T08:00:01.400Z") / 1_000, - error: null, - id: TURN_ID, - items: [], - itemsView: "notLoaded", - startedAt: Date.parse("2026-07-16T08:00:00.000Z") / 1_000, - status: "completed", - }, - }); - - const snapshot = harness.snapshot(); - expect(snapshot.runs).toMatchObject([ - { finishReason: "success", id: RUN_ID, status: "completed" }, - ]); - expect(snapshot.items.map((item) => itemSchema.parse(item))).toMatchObject([ - { - content: [{ text: "hello", type: "text" }], - id: "message-1", - kind: "message", - phase: "final", - role: "agent", - status: "completed", - }, - ]); - - await harness.adapter.handleNotification("item/agentMessage/delta", { - delta: "late", - itemId: "message-1", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - expect(harness.previews).toHaveLength(2); - }); -}); diff --git a/tests/openai-contract-adapter-run-lifecycle.test.ts b/tests/openai-contract-adapter-run-lifecycle.test.ts deleted file mode 100644 index 51722bd..0000000 --- a/tests/openai-contract-adapter-run-lifecycle.test.ts +++ /dev/null @@ -1,632 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - ProtocolAdmissionLimits, - Run, - SessionSnapshot, -} from "../src/contract"; -import { - OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION, - OpenAiContractAdapter, - type OpenAiAuthorityUpdate, -} from "../src/runtimes/openai/contract-adapter"; -import type { ContractPreviewUpdate } from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const COMMAND_ID = protocolId(3); -const THREAD_ID = "thread-1"; -const TURN_ID = "turn-1"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - [OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION]: {}, - "interaction.input": {}, - "interaction.permission": {}, - "interaction.tool": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - "openai.app-server/thread-item": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - options: { - admissionLimits?: ProtocolAdmissionLimits; - holdAuthority?: boolean; - interactionTimeoutMs?: number; - maxPendingServerRequestBytes?: number; - previewCheckpointBytes?: number; - previewReplaceIntervalMs?: number; - } = {}, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - let rejectNextAuthorityAfterCommit = false; - const authorityEntered = Promise.withResolvers(); - const authorityGate = Promise.withResolvers(); - const authority: OpenAiAuthorityUpdate[] = []; - const committedMutationIds = new Set(); - const previews: ContractPreviewUpdate[] = []; - const commit = ( - cause: CommittedMutation["cause"], - operations: AuthorityOperation[], - mutationId = protocolId(1_000 + snapshot.revision + 1), - ): void => { - if (committedMutationIds.has(mutationId)) { - return; - } - - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId, - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - committedMutationIds.add(mutationId); - }; - const adapter = new OpenAiContractAdapter({ - admissionLimits: options.admissionLimits, - authority: async (update) => { - authority.push(update); - if (options.holdAuthority === true) { - authorityEntered.resolve(); - await authorityGate.promise; - } - commit(update.cause, [...update.operations] as AuthorityOperation[], update.mutationId); - if (rejectNextAuthorityAfterCommit) { - rejectNextAuthorityAfterCommit = false; - throw new AuthorityOutcomeUnknownError("authority result lost"); - } - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs: options.interactionTimeoutMs, - maxPendingServerRequestBytes: options.maxPendingServerRequestBytes, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - previewCheckpointBytes: options.previewCheckpointBytes, - previewReplaceIntervalMs: options.previewReplaceIntervalMs, - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - authorityEntered: authorityEntered.promise, - previews, - rejectNextAuthorityAfterCommit() { - rejectNextAuthorityAfterCommit = true; - }, - releaseAuthority() { - authorityGate.resolve(); - }, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - const authoritativeResolution = - resolution?.kind === "input" && resolution.value.type === "answered" - ? { - answeredQuestionIds: Object.keys(resolution.value.answers), - type: "answered" as const, - } - : resolution?.value; - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: authoritativeResolution, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -function turnAttachment() { - return { - cause: { commandId: COMMAND_ID, type: "command" as const }, - run: activeRun("2026-07-16T08:00:00.000Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }; -} - -async function registerTurn(adapter: OpenAiContractAdapter): Promise { - await adapter.attachTurn(turnAttachment()); -} - -function userInputRequest(autoResolutionMs: number | null = null) { - return { - autoResolutionMs, - itemId: "input-1", - questions: [ - { - id: "name", - isOther: false, - isSecret: false, - options: null, - question: "Name?", - }, - ], - threadId: THREAD_ID, - turnId: TURN_ID, - }; -} - -describe("OpenAI Contract adapter", () => { - test("snapshots protected native request data before waiting for a decision", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - const params = { - environmentId: null, - itemId: "permission-1", - permissions: { fileSystem: { read: ["/safe"] } }, - reason: "Read a directory", - threadId: THREAD_ID, - turnId: TURN_ID, - }; - const interactionId = await harness.adapter.handleServerRequest( - "item/permissions/requestApproval", - 53, - params, - ); - params.permissions.fileSystem.read[0] = "/changed"; - const resolution = { - kind: "permission", - value: { optionId: "accept_once", type: "selected" }, - } satisfies InteractionResolution; - harness.settleInteraction(interactionId!, resolution); - - expect(harness.adapter.resolveInteraction(interactionId!, resolution)).toEqual({ - id: 53, - result: { - permissions: { fileSystem: { read: ["/safe"] } }, - scope: "turn", - }, - }); - }); - - test("rejects a native request before commit when protected pending data exceeds its budget", async () => { - const harness = createHarness({ maxPendingServerRequestBytes: 1 }); - await registerTurn(harness.adapter); - const before = harness.authority.length; - - await expect( - harness.adapter.handleServerRequest("item/tool/requestUserInput", 52, userInputRequest()), - ).rejects.toThrow("pending request budget"); - expect(harness.authority).toHaveLength(before); - expect(harness.snapshot().interactions).toHaveLength(0); - }); - - test("projects generated images as stable Tool items with renderable output", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { - id: "image-1", - result: "", - revisedPrompt: null, - status: "inProgress", - type: "imageGeneration", - }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.200Z"), - item: { - id: "image-1", - result: "aGVsbG8=", - revisedPrompt: "A blue whale", - savedPath: "/workspace/whale.png", - status: "completed", - type: "imageGeneration", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - expect(harness.snapshot().items.find((item) => item.id === "image-1")).toMatchObject({ - input: { revisedPrompt: "A blue whale" }, - kind: "tool", - locations: [{ path: "/workspace/whale.png" }], - name: "image_generation", - output: [{ data: "aGVsbG8=", mediaType: "image/png", type: "inline_blob" }], - status: "completed", - }); - }); - - test.each([ - [ - "encoded Item", - { maxBytes: 1_500, maxInlineBytes: 1_024 }, - { - item: { id: "message-oversized", text: "x".repeat(2_048), type: "agentMessage" }, - threadId: THREAD_ID, - turnId: TURN_ID, - }, - "encoded byte limit", - ], - [ - "inline Blob", - { maxBytes: 10_000, maxInlineBytes: 1 }, - { - item: { - id: "image-oversized", - result: "aGVsbG8=", - revisedPrompt: null, - status: "completed", - type: "imageGeneration", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }, - "inline Blob", - ], - ] as const)( - "rejects an oversized projected %s before Authority", - async (_, limits, params, error) => { - const harness = createHarness({ admissionLimits: limits }); - await registerTurn(harness.adapter); - const before = harness.authority.length; - - await expect(harness.adapter.handleNotification("item/completed", params)).rejects.toThrow( - error, - ); - expect(harness.authority).toHaveLength(before); - expect(harness.snapshot().items).toHaveLength(0); - }, - ); - - test("preserves multi-agent routing and status in Agent Tool items", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - const base = { - agentsStates: {}, - id: "agent-call-1", - model: "gpt-5", - prompt: "Inspect the adapter", - reasoningEffort: "high", - receiverThreadIds: ["child-1"], - senderThreadId: THREAD_ID, - tool: "spawnAgent", - type: "collabAgentToolCall", - }; - await harness.adapter.handleNotification("item/started", { - item: { ...base, status: "inProgress" }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.200Z"), - item: { - ...base, - agentsStates: { "child-1": { message: "Done", status: "completed" } }, - status: "completed", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - expect(harness.snapshot().items.find((item) => item.id === "agent-call-1")).toMatchObject({ - category: "agent", - input: { - prompt: "Inspect the adapter", - receiverThreadIds: ["child-1"], - senderThreadId: THREAD_ID, - }, - kind: "tool", - name: "spawnAgent", - status: "completed", - structuredOutput: { "child-1": { message: "Done", status: "completed" } }, - }); - }); - - test("atomically expires unresolved interactions before a cancelled Run", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { - aggregatedOutput: "", - command: "sleep 10", - id: "command-1", - status: "inProgress", - type: "commandExecution", - }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleServerRequest("item/commandExecution/requestApproval", 9, { - command: "sleep 10", - itemId: "command-1", - startedAtMs: Date.parse("2026-07-16T08:00:00.200Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleServerRequest("mcpServer/elicitation/request", 90, { - _meta: null, - message: "Confirm", - mode: "url", - elicitationId: "external-1", - serverName: "deployment", - threadId: THREAD_ID, - turnId: TURN_ID, - url: "https://example.com/confirm", - }); - await harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { - completedAt: Date.parse("2026-07-16T08:00:00.500Z") / 1_000, - error: null, - id: TURN_ID, - items: [], - itemsView: "notLoaded", - startedAt: Date.parse("2026-07-16T08:00:00.000Z") / 1_000, - status: "interrupted", - }, - }); - - expect(harness.snapshot().runs[0]).toMatchObject({ id: RUN_ID, status: "cancelled" }); - expect(harness.snapshot().items[0]).toMatchObject({ - id: "command-1", - status: "cancelled", - }); - expect(harness.snapshot().interactions).toEqual([ - expect.objectContaining({ kind: "permission", status: "expired" }), - expect.objectContaining({ kind: "extension", status: "expired" }), - ]); - expect( - harness.snapshot().interactions.every((interaction) => interaction.resolution === undefined), - ).toBe(true); - }); - - test("expires an overdue interaction while closing its Run", async () => { - const harness = createHarness({ interactionTimeoutMs: 100 }); - await registerTurn(harness.adapter); - await harness.adapter.handleServerRequest("item/tool/requestUserInput", 10, { - autoResolutionMs: null, - itemId: "input-1", - questions: [ - { - header: "Name", - id: "name", - isOther: false, - isSecret: false, - options: null, - question: "Your name?", - }, - ], - threadId: THREAD_ID, - turnId: TURN_ID, - }); - harness.advance(101); - await harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { - completedAt: Date.parse("2026-07-16T08:00:00.101Z") / 1_000, - error: null, - id: TURN_ID, - items: [], - itemsView: "full", - startedAt: Date.parse("2026-07-16T08:00:00.000Z") / 1_000, - status: "completed", - }, - }); - - expect(harness.snapshot().interactions[0]).toMatchObject({ - status: "expired", - }); - expect(harness.snapshot().interactions[0]).not.toHaveProperty("resolution"); - }); - - test("closes an Interaction when app-server reports its request resolved elsewhere", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - const interactionId = await harness.adapter.handleServerRequest( - "item/tool/requestUserInput", - 11, - { - autoResolutionMs: null, - itemId: "input-1", - questions: [ - { - header: "Name", - id: "name", - isOther: false, - isSecret: false, - options: [], - question: "Your name?", - }, - ], - threadId: THREAD_ID, - turnId: TURN_ID, - }, - ); - - expect( - harness.snapshot().interactions.find((interaction) => interaction.id === interactionId), - ).toMatchObject({ - request: { questions: [{ id: "name", type: "text" }] }, - status: "open", - }); - - await harness.adapter.handleNotification("serverRequest/resolved", { - requestId: 11, - threadId: THREAD_ID, - }); - - expect( - harness.snapshot().interactions.find((interaction) => interaction.id === interactionId), - ).toMatchObject({ status: "expired" }); - expect( - harness.adapter.resolveInteraction(interactionId!, { - kind: "input", - value: { type: "cancelled" }, - }), - ).toBeNull(); - }); - - test("fails closed when a successful Turn omits an active Item snapshot", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { - aggregatedOutput: "", - command: "pwd", - id: "command-1", - status: "inProgress", - type: "commandExecution", - }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { - completedAt: Date.parse("2026-07-16T08:00:00.200Z") / 1_000, - error: null, - id: TURN_ID, - items: [], - itemsView: "full", - startedAt: Date.parse("2026-07-16T08:00:00.000Z") / 1_000, - status: "completed", - }, - }); - - expect(harness.snapshot().items[0]).toMatchObject({ - error: { code: "openai.turn.incomplete" }, - status: "failed", - }); - expect(harness.snapshot().runs[0]).toMatchObject({ - error: { code: "openai.turn.incomplete" }, - status: "failed", - }); - }); - - test("uses receipt time instead of untrusted provider lifecycle clocks", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { id: "message-1", text: "", type: "agentMessage" }, - startedAtMs: Date.parse("2099-01-01T00:00:00.000Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2000-01-01T00:00:00.000Z"), - item: { id: "message-1", text: "done", type: "agentMessage" }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { - completedAt: Date.parse("1999-01-01T00:00:00.000Z") / 1_000, - error: null, - id: TURN_ID, - items: [], - itemsView: "notLoaded", - startedAt: Date.parse("2099-01-01T00:00:00.000Z") / 1_000, - status: "completed", - }, - }); - - expect(harness.snapshot().items[0]).toMatchObject({ - createdAt: "2026-07-16T08:00:00.000Z", - endedAt: "2026-07-16T08:00:00.000Z", - updatedAt: "2026-07-16T08:00:00.000Z", - }); - expect(harness.snapshot().runs[0]).toMatchObject({ - endedAt: "2026-07-16T08:00:00.000Z", - status: "completed", - }); - }); - - test("disposal releases all in-memory projection state", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - harness.adapter.dispose(); - - await expect( - harness.adapter.handleNotification("turn/completed", { - threadId: THREAD_ID, - turn: { id: TURN_ID, status: "completed" }, - }), - ).rejects.toThrow("disposed"); - }); - - test.each([Number.POSITIVE_INFINITY, 1.5])("rejects invalid resource limit %p", (value) => { - expect(() => createHarness({ previewCheckpointBytes: value })).toThrow("finite and positive"); - }); -}); diff --git a/tests/openai-contract-adapter-terminal.test.ts b/tests/openai-contract-adapter-terminal.test.ts deleted file mode 100644 index eb9442e..0000000 --- a/tests/openai-contract-adapter-terminal.test.ts +++ /dev/null @@ -1,620 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - AuthorityOutcomeUnknownError, - applyCommittedMutation, - interactionSchema, - validateSessionSnapshot, -} from "../src/contract"; -import type { - AuthorityOperation, - CommittedMutation, - InteractionResolution, - ProtocolAdmissionLimits, - Run, - SessionSnapshot, -} from "../src/contract"; -import { - OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION, - OpenAiContractAdapter, - type OpenAiAuthorityUpdate, -} from "../src/runtimes/openai/contract-adapter"; -import type { ContractPreviewUpdate } from "../src/runtimes/contract-projection"; - -const SESSION_ID = protocolId(1); -const RUN_ID = protocolId(2); -const COMMAND_ID = protocolId(3); -const THREAD_ID = "thread-1"; -const TURN_ID = "turn-1"; - -function protocolId(value: number): string { - return value.toString().padStart(26, "0"); -} - -function activeRun(startedAt: string): Run { - return { - id: RUN_ID, - input: [{ text: "hello", type: "text" }], - origin: "user", - startedAt, - status: "active", - }; -} - -function createInitialSnapshot(capturedAt: string): SessionSnapshot { - return validateSessionSnapshot({ - capturedAt, - interactions: [], - items: [], - protocolVersion: 2, - revision: 0, - runs: [activeRun(capturedAt)], - session: { - capabilities: { - [OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION]: {}, - "interaction.input": {}, - "interaction.permission": {}, - "interaction.tool": {}, - "item.change": {}, - "item.plan": {}, - "item.reasoning": {}, - "item.terminal": {}, - "openai.app-server/thread-item": {}, - }, - config: [], - createdAt: capturedAt, - id: SESSION_ID, - status: "open", - updatedAt: capturedAt, - }, - }); -} - -function createHarness( - options: { - admissionLimits?: ProtocolAdmissionLimits; - holdAuthority?: boolean; - interactionTimeoutMs?: number; - maxPendingServerRequestBytes?: number; - previewCheckpointBytes?: number; - previewReplaceIntervalMs?: number; - } = {}, -) { - let nowMs = Date.parse("2026-07-16T08:00:00.000Z"); - let snapshot = createInitialSnapshot(new Date(nowMs).toISOString()); - let nextId = 100; - let rejectNextAuthorityAfterCommit = false; - const authorityEntered = Promise.withResolvers(); - const authorityGate = Promise.withResolvers(); - const authority: OpenAiAuthorityUpdate[] = []; - const committedMutationIds = new Set(); - const previews: ContractPreviewUpdate[] = []; - const commit = ( - cause: CommittedMutation["cause"], - operations: AuthorityOperation[], - mutationId = protocolId(1_000 + snapshot.revision + 1), - ): void => { - if (committedMutationIds.has(mutationId)) { - return; - } - - const revision = snapshot.revision + 1; - const mutation: CommittedMutation = { - baseRevision: snapshot.revision, - cause, - committedAt: new Date(nowMs).toISOString(), - mutationId, - operations, - revision, - sessionId: SESSION_ID, - }; - snapshot = applyCommittedMutation(snapshot, mutation); - committedMutationIds.add(mutationId); - }; - const adapter = new OpenAiContractAdapter({ - admissionLimits: options.admissionLimits, - authority: async (update) => { - authority.push(update); - if (options.holdAuthority === true) { - authorityEntered.resolve(); - await authorityGate.promise; - } - commit(update.cause, [...update.operations] as AuthorityOperation[], update.mutationId); - if (rejectNextAuthorityAfterCommit) { - rejectNextAuthorityAfterCommit = false; - throw new AuthorityOutcomeUnknownError("authority result lost"); - } - }, - createId: () => protocolId(nextId++), - interactionTimeoutMs: options.interactionTimeoutMs, - maxPendingServerRequestBytes: options.maxPendingServerRequestBytes, - now: () => new Date(nowMs), - preview: (update) => previews.push(update), - previewCheckpointBytes: options.previewCheckpointBytes, - previewReplaceIntervalMs: options.previewReplaceIntervalMs, - sessionId: SESSION_ID, - }); - - return { - adapter, - advance(milliseconds: number) { - nowMs += milliseconds; - }, - authority, - authorityEntered: authorityEntered.promise, - previews, - rejectNextAuthorityAfterCommit() { - rejectNextAuthorityAfterCommit = true; - }, - releaseAuthority() { - authorityGate.resolve(); - }, - settleInteraction(interactionId: string, resolution?: InteractionResolution) { - const interaction = snapshot.interactions.find((entry) => entry.id === interactionId); - - if (interaction === undefined || interaction.status !== "open") { - throw new Error("The test interaction must be open."); - } - - if (resolution !== undefined && resolution.kind !== interaction.kind) { - throw new Error("The test resolution kind must match the interaction kind."); - } - - const endedAt = new Date(nowMs).toISOString(); - const authoritativeResolution = - resolution?.kind === "input" && resolution.value.type === "answered" - ? { - answeredQuestionIds: Object.keys(resolution.value.answers), - type: "answered" as const, - } - : resolution?.value; - commit({ commandId: protocolId(2_000 + snapshot.revision + 1), type: "command" }, [ - { - entity: "interaction", - op: "put", - value: interactionSchema.parse( - resolution === undefined - ? { ...interaction, endedAt, status: "expired" } - : { - ...interaction, - endedAt, - resolution: authoritativeResolution, - status: "resolved", - }, - ), - }, - ]); - }, - snapshot: () => snapshot, - }; -} - -function turnAttachment() { - return { - cause: { commandId: COMMAND_ID, type: "command" as const }, - run: activeRun("2026-07-16T08:00:00.000Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }; -} - -async function registerTurn(adapter: OpenAiContractAdapter): Promise { - await adapter.attachTurn(turnAttachment()); -} - -function userInputRequest(autoResolutionMs: number | null = null) { - return { - autoResolutionMs, - itemId: "input-1", - questions: [ - { - id: "name", - isOther: false, - isSecret: false, - options: null, - question: "Name?", - }, - ], - threadId: THREAD_ID, - turnId: TURN_ID, - }; -} - -describe("OpenAI Contract adapter", () => { - test("uses an empty file-change snapshot to clear prior active changes", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/fileChange/patchUpdated", { - changes: [{ diff: "+old", kind: { type: "add" }, path: "old.txt" }], - itemId: "change-clear", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - await harness.adapter.handleNotification("item/fileChange/patchUpdated", { - changes: [], - itemId: "change-clear", - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - expect(harness.snapshot().items).toContainEqual( - expect.objectContaining({ changes: [], id: "change-clear", status: "active" }), - ); - }); - - test("retains sub-agent activity input", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { - agentPath: "researcher", - agentThreadId: "child-thread", - id: "sub-agent-1", - kind: "spawn", - status: "inProgress", - type: "subAgentActivity", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - expect(harness.snapshot().items).toContainEqual( - expect.objectContaining({ - input: { - agentPath: "researcher", - agentThreadId: "child-thread", - kind: "spawn", - }, - name: "sub_agent_activity", - }), - ); - }); - - test("preserves unrecognized native Item data in an Extension Item", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/completed", { - item: { - id: "review-1", - review: "Check authentication boundaries.", - type: "enteredReviewMode", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - - expect(harness.snapshot().items[0]).toMatchObject({ - kind: "extension", - status: "completed", - value: { - id: "review-1", - review: "Check authentication boundaries.", - type: "enteredReviewMode", - }, - }); - }); - - test("projects interactive server requests and maps protected resolutions back to app-server", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - await harness.adapter.handleNotification("item/started", { - item: { - aggregatedOutput: null, - command: "git status", - id: "command-1", - status: "inProgress", - type: "commandExecution", - }, - startedAtMs: Date.parse("2026-07-16T08:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - const approvalId = await harness.adapter.handleServerRequest( - "item/commandExecution/requestApproval", - 7, - { - command: "git status", - itemId: "command-1", - reason: "Needs approval", - startedAtMs: Date.parse("2026-07-16T08:00:00.200Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }, - ); - - expect(approvalId).not.toBeNull(); - expect( - interactionSchema.parse( - harness.snapshot().interactions.find((interaction) => interaction.id === approvalId), - ), - ).toMatchObject({ - itemId: "command-1", - kind: "permission", - status: "open", - }); - const approvalResolution = { - kind: "permission", - value: { optionId: "accept_session", type: "selected" }, - } satisfies InteractionResolution; - harness.settleInteraction(approvalId!, approvalResolution); - expect(harness.adapter.resolveInteraction(approvalId!, approvalResolution)).toEqual({ - id: 7, - result: { decision: "acceptForSession" }, - }); - - const restrictedApprovalId = await harness.adapter.handleServerRequest( - "item/commandExecution/requestApproval", - 70, - { - availableDecisions: ["accept", "decline"], - command: "git status", - itemId: "command-1", - reason: "Needs approval", - startedAtMs: Date.parse("2026-07-16T08:00:00.200Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }, - ); - expect( - harness - .snapshot() - .interactions.find((interaction) => interaction.id === restrictedApprovalId), - ).toMatchObject({ - request: { - options: [{ id: "accept_once" }, { id: "decline" }], - }, - }); - expect(() => - harness.adapter.resolveInteraction(restrictedApprovalId!, { - kind: "permission", - value: { optionId: "accept_session", type: "selected" }, - }), - ).toThrow("unavailable option"); - const restrictedResolution = { - kind: "permission", - value: { optionId: "accept_once", type: "selected" }, - } satisfies InteractionResolution; - harness.settleInteraction(restrictedApprovalId!, restrictedResolution); - expect(harness.adapter.resolveInteraction(restrictedApprovalId!, restrictedResolution)).toEqual( - { id: 70, result: { decision: "accept" } }, - ); - - const inputId = await harness.adapter.handleServerRequest("item/tool/requestUserInput", 8, { - autoResolutionMs: 60_000, - itemId: "command-1", - questions: [ - { - header: "Mode", - id: "mode", - isOther: true, - isSecret: false, - options: [{ description: "Fast", label: "quick" }], - question: "Which mode?", - }, - ], - threadId: THREAD_ID, - turnId: TURN_ID, - }); - const inputResolution: InteractionResolution = { - kind: "input", - value: { answers: { mode: ["0"] }, type: "answered" }, - }; - expect( - harness.snapshot().interactions.find((interaction) => interaction.id === inputId), - ).toMatchObject({ - kind: "input", - request: { - questions: [ - { - allowOther: true, - options: [{ id: "0", label: "quick" }], - }, - ], - }, - }); - harness.settleInteraction(inputId!, inputResolution); - expect(harness.adapter.resolveInteraction(inputId!, inputResolution)).toEqual({ - id: 8, - result: { answers: { mode: { answers: ["quick"] } } }, - }); - - await harness.adapter.handleNotification("item/started", { - item: { - arguments: { path: "README.md" }, - contentItems: null, - durationMs: null, - id: "call-1", - namespace: "fs", - status: "inProgress", - success: null, - tool: "read_file", - type: "dynamicToolCall", - }, - startedAtMs: Date.parse("2026-07-16T08:00:00.300Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - }); - const toolId = await harness.adapter.handleServerRequest("item/tool/call", "tool-request", { - arguments: { path: "README.md" }, - callId: "call-1", - namespace: "fs", - threadId: THREAD_ID, - tool: "read_file", - turnId: TURN_ID, - }); - expect( - harness.snapshot().interactions.find((interaction) => interaction.id === toolId), - ).toMatchObject({ - itemId: "call-1", - }); - const toolResolution = { - kind: "tool", - value: { - output: [{ text: "contents", type: "text" }], - type: "completed", - }, - } satisfies InteractionResolution; - harness.settleInteraction(toolId!, toolResolution); - expect(harness.adapter.resolveInteraction(toolId!, toolResolution)).toEqual({ - id: "tool-request", - result: { - contentItems: [{ text: "contents", type: "inputText" }], - success: true, - }, - }); - await harness.adapter.handleNotification("item/completed", { - completedAtMs: Date.parse("2026-07-16T08:00:00.400Z"), - item: { - arguments: { path: "README.md" }, - contentItems: [{ text: "contents", type: "inputText" }], - durationMs: 100, - id: "call-1", - namespace: "fs", - status: "completed", - success: true, - tool: "read_file", - type: "dynamicToolCall", - }, - threadId: THREAD_ID, - turnId: TURN_ID, - }); - expect(harness.snapshot().items.find((item) => item.id === "call-1")).toMatchObject({ - kind: "tool", - name: "fs/read_file", - output: [{ text: "contents", type: "text" }], - status: "completed", - }); - - const elicitationId = await harness.adapter.handleServerRequest( - "mcpServer/elicitation/request", - "elicitation-request", - { - _meta: null, - message: "Choose a region", - mode: "form", - requestedSchema: { - properties: { region: { type: "string" } }, - required: ["region"], - type: "object", - }, - serverName: "deployment", - threadId: THREAD_ID, - turnId: TURN_ID, - }, - ); - expect( - harness.snapshot().interactions.find((interaction) => interaction.id === elicitationId), - ).toMatchObject({ - kind: "extension", - name: OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION, - request: { message: "Choose a region", mode: "form", serverName: "deployment" }, - }); - const elicitationResolution = { - kind: "extension", - name: OPENAI_APP_SERVER_MCP_ELICITATION_EXTENSION, - value: { _meta: null, action: "accept", content: { region: "eu" } }, - } satisfies InteractionResolution; - harness.settleInteraction(elicitationId!, elicitationResolution); - expect(harness.adapter.resolveInteraction(elicitationId!, elicitationResolution)).toEqual({ - id: "elicitation-request", - result: { _meta: null, action: "accept", content: { region: "eu" } }, - }); - expect( - harness.snapshot().interactions.every((interaction) => interaction.status !== "open"), - ).toBe(true); - }); - - test.each([ - { expectedMs: 500, requestedMs: 500 }, - { expectedMs: 1_000, requestedMs: 5_000 }, - { expectedMs: 1_000, requestedMs: 1.5 }, - { expectedMs: 1_000, requestedMs: null }, - ])( - "bounds provider interaction timeout $requestedMs -> $expectedMs ms", - async ({ expectedMs, requestedMs }) => { - const harness = createHarness({ interactionTimeoutMs: 1_000 }); - await registerTurn(harness.adapter); - await harness.adapter.handleServerRequest( - "item/tool/requestUserInput", - 50, - userInputRequest(requestedMs), - ); - - expect(harness.snapshot().interactions[0]?.expiresAt).toBe( - new Date(Date.parse("2026-07-16T08:00:00.000Z") + expectedMs).toISOString(), - ); - }, - ); - - test("deduplicates a pending native request before creating Authority state", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - const before = harness.authority.length; - const first = await harness.adapter.handleServerRequest( - "item/tool/requestUserInput", - 51, - userInputRequest(), - ); - const second = await harness.adapter.handleServerRequest( - "item/tool/requestUserInput", - 51, - userInputRequest(), - ); - - expect(second).toBe(first); - expect(harness.authority).toHaveLength(before + 1); - expect(harness.snapshot().interactions).toHaveLength(1); - }); - - test("keeps a native request identity after an ambiguous Authority result", async () => { - const harness = createHarness(); - await registerTurn(harness.adapter); - const request = userInputRequest(); - harness.rejectNextAuthorityAfterCommit(); - - await expect( - harness.adapter.handleServerRequest("item/tool/requestUserInput", 52, request), - ).rejects.toThrow("result lost"); - const interactionId = harness.snapshot().interactions[0]?.id; - await expect( - harness.adapter.handleServerRequest("item/tool/requestUserInput", 52, request), - ).resolves.toBe(interactionId); - - expect(harness.snapshot().interactions).toHaveLength(1); - expect( - harness.authority - .slice(-2) - .map((update) => update.operations[0]) - .map((operation) => (operation?.op === "put" ? operation.value.id : null)), - ).toEqual([interactionId, interactionId]); - }); - - test.each([ - [ - "item identity", - (request: ReturnType) => ({ ...request, itemId: "input-2" }), - ], - [ - "question payload", - (request: ReturnType) => ({ - ...request, - questions: [{ ...request.questions[0]!, question: "Changed?" }], - }), - ], - ] as const)( - "rejects a replay that changes the pending native request %s", - async (_name, change) => { - const harness = createHarness(); - await registerTurn(harness.adapter); - const request = userInputRequest(); - await harness.adapter.handleServerRequest("item/tool/requestUserInput", 51, request); - const before = harness.authority.length; - - await expect( - harness.adapter.handleServerRequest("item/tool/requestUserInput", 51, change(request)), - ).rejects.toThrow("changed identity"); - expect(harness.authority).toHaveLength(before); - expect(harness.snapshot().interactions).toHaveLength(1); - }, - ); -}); diff --git a/tests/opencode-acp-contract.test.ts b/tests/opencode-acp-contract.test.ts index a217183..dd13bac 100644 --- a/tests/opencode-acp-contract.test.ts +++ b/tests/opencode-acp-contract.test.ts @@ -108,7 +108,7 @@ async function stopOpenCode( } } -test("OpenCode creates a session after unadvertised additional directories are dropped", async () => { +test("OpenCode rejects unadvertised additional directories before session creation", async () => { expect(existsSync(OPENCODE_COMMAND)).toBe(true); const root = await mkdtemp(join(tmpdir(), "agent-driver-opencode-acp-contract-")); @@ -184,7 +184,7 @@ test("OpenCode creates a session after unadvertised additional directories are d runtime: "acp-fallback", runtimeTransport: "acp-fallback", }; - const setup = await requestWithTimeout(connection, "OpenCode ACP session/new", () => + await expect( setupAcpSession({ agentCapabilities: initialize.agentCapabilities ?? null, connection: connection.agent, @@ -193,11 +193,7 @@ test("OpenCode creates a session after unadvertised additional directories are d sessionContext, replaySession: async (operation) => operation(), }), - ); - - expect(setup.mode).toBe("created"); - expect(setup.sessionId.trim().length).toBeGreaterThan(0); - expect(setup.droppedAdditionalDirectories).toEqual([additionalDirectory]); + ).rejects.toThrow("does not advertise additionalDirectories support"); } finally { await stopOpenCode(connection, child, closed); await rm(root, { force: true, recursive: true }); diff --git a/tests/provider-registry.test.ts b/tests/provider-registry.test.ts index b54fb32..2790306 100644 --- a/tests/provider-registry.test.ts +++ b/tests/provider-registry.test.ts @@ -79,7 +79,7 @@ describe("provider registry", () => { { id: "input_start", status: "supported", version: 1 }, { id: "permission_request", status: "unsupported", version: 1 }, { id: "session_stop", status: "supported", version: 1 }, - { id: "thinking_stream", status: "unsupported", version: 1 }, + { id: "thinking_stream", status: "supported", version: 1 }, ]), ); }); diff --git a/tests/public-api-exports.snapshot.json b/tests/public-api-exports.snapshot.json index e9e629a..a8e25cb 100644 --- a/tests/public-api-exports.snapshot.json +++ b/tests/public-api-exports.snapshot.json @@ -86,6 +86,7 @@ "DriverInstanceId", "DriverNativeRuntimeRef", "DriverNativeRuntimeRefKind", + "DriverPermissionRequest", "DriverRuntime", "DriverRuntimeTransport", "DriverStartInput", diff --git a/tests/public-api-exports.test.ts b/tests/public-api-exports.test.ts index 710bd84..c5b2d2d 100644 --- a/tests/public-api-exports.test.ts +++ b/tests/public-api-exports.test.ts @@ -1,22 +1,15 @@ import { expect, test } from "bun:test"; import { readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; -import * as ts from "typescript"; +import { API, SymbolFlags, type Project } from "typescript/unstable/async"; const repositoryRoot = fileURLToPath(new URL("../", import.meta.url)); const snapshotPath = resolve(repositoryRoot, "tests/public-api-exports.snapshot.json"); interface PackageManifest { - readonly exports: Readonly< - Record< - string, - { - readonly default: string; - } - > - >; + readonly exports: Readonly>; } interface EntryExports { @@ -26,94 +19,105 @@ interface EntryExports { type ExportSnapshot = Readonly>; -function compilerProgram(): ts.Program { - const configPath = resolve(repositoryRoot, "tsconfig.json"); - const config = ts.readConfigFile(configPath, (path) => readFileSync(path, "utf8")); - - if (config.error !== undefined) { - throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, "\n")); - } - - const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, repositoryRoot); - return ts.createProgram(parsed.fileNames, parsed.options); -} - -function isTypeOnlyAlias(symbol: ts.Symbol): boolean { - const declarations = symbol.declarations; - - return ( - declarations !== undefined && - declarations.length > 0 && - declarations.every((declaration) => { - if (!ts.isExportSpecifier(declaration)) { - return false; - } - - return declaration.isTypeOnly || declaration.parent.parent.isTypeOnly; - }) - ); -} - -function collectEntryExports(program: ts.Program, entryPath: string): EntryExports { - const sourceFile = program.getSourceFile(entryPath); +async function collectEntryExports(project: Project, entryPath: string): Promise { + const source = await project.program.getSourceFile(entryPath); - if (sourceFile === undefined) { - throw new Error(`Public package entry is missing from the TypeScript program: ${entryPath}.`); + if (source === undefined) { + throw new Error(`Public package entry is missing: ${entryPath}.`); } - const checker = program.getTypeChecker(); - const moduleSymbol = checker.getSymbolAtLocation(sourceFile); + const values = new Set(Object.keys(await import(pathToFileURL(entryPath).href))); + const moduleSymbol = await project.checker.getSymbolAtLocation(source); if (moduleSymbol === undefined) { - return { types: [], values: [] }; + throw new Error(`Public package entry has no module symbol: ${entryPath}.`); } const types = new Set(); - const values = new Set(); - for (const exported of checker.getExportsOfModule(moduleSymbol)) { + for (const exported of await project.checker.getExportsOfModule(moduleSymbol)) { const target = - (exported.flags & ts.SymbolFlags.Alias) === 0 ? exported : checker.getAliasedSymbol(exported); - const typeOnly = isTypeOnlyAlias(exported); + exported.flags & SymbolFlags.Alias + ? await project.checker.getAliasedSymbol(exported) + : exported; - if ((target.flags & ts.SymbolFlags.Type) !== 0) { - types.add(exported.name); + if (await project.checker.isUnknownSymbol(target)) { + throw new Error(`Public API export ${exported.name} is unresolved.`); } - if (!typeOnly && (target.flags & ts.SymbolFlags.Value) !== 0) { - values.add(exported.name); + if (target.flags & (SymbolFlags.Type | SymbolFlags.Namespace)) { + types.add(exported.name); } } - return { - types: [...types].toSorted(), - values: [...values].toSorted(), - }; + return { types: [...types].toSorted(), values: [...values].toSorted() }; } -function collectPublicExports(): ExportSnapshot { - const manifest = JSON.parse( - readFileSync(resolve(repositoryRoot, "package.json"), "utf8"), - ) as PackageManifest; - const program = compilerProgram(); - - return Object.fromEntries( - Object.entries(manifest.exports) - .map(([entry, target]): [string, EntryExports] => [ - entry, - collectEntryExports(program, resolve(repositoryRoot, target.default)), - ]) - .toSorted(([left], [right]) => left.localeCompare(right)), - ); -} +test("public package entries preserve their complete value and type export sets", async () => { + const configPath = resolve(repositoryRoot, "tsconfig.json"); + const api = new API({ cwd: repositoryRoot }); -test("public package entries preserve their complete value and type export sets", () => { - const current = collectPublicExports(); + try { + const snapshot = await api.updateSnapshot({ openProjects: [configPath] }); + const project = snapshot.getProject(configPath); - if (process.env["UPDATE_PUBLIC_EXPORT_SNAPSHOT"] === "1") { - writeFileSync(snapshotPath, `${JSON.stringify(current, null, 2)}\n`); - } + if (project === undefined) { + throw new Error(`TypeScript project is missing: ${configPath}.`); + } + + expect( + await collectEntryExports( + project, + resolve(repositoryRoot, "tests/fixtures/public-api-type-namespace.ts"), + ), + ).toEqual({ + types: [ + "AuditBoth", + "AuditDeclaredNamespace", + "AuditDefault", + "AuditMerged", + "AuditNamespace", + "AuditType", + "AuditTypeNamespace", + "Named", + "TypeStarBoth", + "TypeStarOnly", + "default", + ], + values: [ + "AuditBoth", + "AuditDefault", + "AuditMerged", + "AuditNamespace", + "AuditValue", + "default", + ], + }); + + const manifest = JSON.parse( + readFileSync(resolve(repositoryRoot, "package.json"), "utf8"), + ) as PackageManifest; + const current = Object.fromEntries( + ( + await Promise.all( + Object.entries(manifest.exports).map( + async ([entry, target]) => + [ + entry, + await collectEntryExports(project, resolve(repositoryRoot, target.default)), + ] as const, + ), + ) + ).toSorted(([left], [right]) => left.localeCompare(right)), + ); + + if (process.env["UPDATE_PUBLIC_EXPORT_SNAPSHOT"] === "1") { + writeFileSync(snapshotPath, `${JSON.stringify(current, null, 2)}\n`); + } - const expected = JSON.parse(readFileSync(snapshotPath, "utf8")) as ExportSnapshot; - expect(current).toEqual(expected); + const expected = JSON.parse(readFileSync(snapshotPath, "utf8")) as ExportSnapshot; + expect(current).toEqual(expected); + } finally { + await api.close(); + } }); diff --git a/tests/remote-http-mcp-executor.test.ts b/tests/remote-http-mcp-executor.test.ts new file mode 100644 index 0000000..1c5df0e --- /dev/null +++ b/tests/remote-http-mcp-executor.test.ts @@ -0,0 +1,337 @@ +import { expect, test } from "bun:test"; + +import type { CredentialId, McpServerId } from "../src/protocol/boot"; +import type { DriverStartInput } from "../src/protocol/start"; +import type { McpExecuteCommand } from "../src/runtime-command"; +import { executeRemoteHttpMcpCommand } from "../src/runtimes/mcp/remote-http-mcp-executor"; +import { driverStartInput } from "./driver-boot-payload-fixture"; + +const MCP_SERVER_ID = "01J00000000000000000000020" as McpServerId; +const MCP_CREDENTIAL_ID = "01J00000000000000000000021" as CredentialId; + +function payload(proxyUrl: string): DriverStartInput { + return { + ...driverStartInput, + execution: { + ...driverStartInput.execution, + session: { + ...driverStartInput.execution.session, + mcpServers: [ + { + authType: "bearer", + authorizationState: "active", + credentialId: MCP_CREDENTIAL_ID, + credentialScope: "session", + credentialStatus: "active", + name: "Test MCP", + proxyGrantId: "test-grant", + proxyUrl, + serverId: MCP_SERVER_ID, + }, + ], + }, + }, + }; +} + +function command(requestId: string): McpExecuteCommand { + return { + argumentsJson: "{}", + commandId: `command-${requestId}`, + kind: "mcp.execute", + requestId, + serverId: MCP_SERVER_ID, + toolCallId: `tool-${requestId}`, + toolName: "lookup", + }; +} + +async function execute(proxyUrl: string, requestId: string, signal = new AbortController().signal) { + return executeRemoteHttpMcpCommand(payload(proxyUrl), command(requestId), signal, { + effectId: `effect-${requestId}`, + idempotencyKey: `key-${requestId}`, + }); +} + +test("classifies typed MCP HTTP failures by status", async () => { + const unauthorizedHeaders: Array = []; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch(request) { + const status = Number(new URL(request.url).pathname.slice(1)); + if (status === 401) { + unauthorizedHeaders.push(request.headers.get("authorization")); + } + return new Response("failed", { status }); + }, + }); + + try { + for (const [status, message] of [ + [400, "rejected the request for lookup (HTTP 400)"], + [401, "authorization for Test MCP is no longer valid"], + [403, "rejected the credential for lookup"], + [404, "HTTP endpoint for Test MCP was not found"], + [408, "Timed out while calling MCP tool lookup"], + [409, "reported a conflict while calling lookup"], + [418, "rejected lookup with HTTP 418"], + [429, "rate limited lookup"], + [503, "failed while calling lookup (HTTP 503)"], + ] as const) { + await expect( + execute(`http://${server.hostname}:${server.port}/${status}`, `status-${status}`), + ).rejects.toThrow(message); + } + expect(unauthorizedHeaders).toEqual(["Bearer test-grant"]); + } finally { + await server.stop(true); + } +}); + +test("auto-negotiates the latest MCP era and falls back to legacy", async () => { + const methods = new Map(); + const protocolHeaders: Array = []; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + if (request.method !== "POST") { + return new Response(null, { status: 405 }); + } + + const route = new URL(request.url).pathname.slice(1); + const message = (await request.json()) as { + id?: unknown; + method?: string; + params?: { protocolVersion?: string }; + }; + const method = message.method ?? "unknown"; + methods.set(route, [...(methods.get(route) ?? []), method]); + + if (route === "legacy" && method === "server/discover") { + return new Response("not found", { status: 404 }); + } + + if (method === "server/discover") { + return Response.json({ + id: message.id, + jsonrpc: "2.0", + result: { + capabilities: { tools: {} }, + supportedVersions: ["2026-07-28"], + }, + }); + } + + if (method === "initialize") { + return Response.json({ + id: message.id, + jsonrpc: "2.0", + result: { + capabilities: { tools: {} }, + protocolVersion: message.params?.protocolVersion, + serverInfo: { name: "test-mcp", version: "1" }, + }, + }); + } + + if (method === "notifications/initialized") { + return new Response(null, { status: 202 }); + } + + if (method === "tools/call") { + protocolHeaders.push(request.headers.get("mcp-protocol-version")); + if (route === "scope") { + return new Response("insufficient scope", { + headers: { + "www-authenticate": 'Bearer error="insufficient_scope", scope="tools:call"', + }, + status: 403, + }); + } + if (route === "mixed") { + return Response.json({ + id: message.id, + jsonrpc: "2.0", + result: { + _meta: { receipt: "private" }, + content: [ + { text: " exact text ", type: "text" }, + { data: "aA==", mimeType: "image/png", type: "image" }, + ], + isError: false, + resultType: "complete", + structuredContent: { count: 1 }, + }, + }); + } + if (route === "annotated") { + return Response.json({ + id: message.id, + jsonrpc: "2.0", + result: { + content: [ + { + _meta: { source: "provider" }, + annotations: { audience: ["assistant"] }, + text: "annotated", + type: "text", + }, + ], + resultType: "complete", + }, + }); + } + return Response.json({ + id: message.id, + jsonrpc: "2.0", + result: { + content: [{ text: route, type: "text" }], + ...(route === "modern" ? { resultType: "complete" } : {}), + }, + }); + } + + return new Response("unexpected request", { status: 400 }); + }, + }); + + try { + const baseUrl = `http://${server.hostname}:${server.port}`; + expect(await execute(`${baseUrl}/modern`, "modern")).toMatchObject({ outputText: "modern" }); + expect(await execute(`${baseUrl}/legacy`, "legacy")).toMatchObject({ outputText: "legacy" }); + const mixed = await execute(`${baseUrl}/mixed`, "mixed"); + expect(JSON.parse(mixed.outputText)).toEqual({ + content: [ + { text: " exact text ", type: "text" }, + { data: "aA==", mimeType: "image/png", type: "image" }, + ], + isError: false, + structuredContent: { count: 1 }, + }); + expect(mixed.providerReceiptJson).toBe('{"receipt":"private"}'); + expect(JSON.parse((await execute(`${baseUrl}/annotated`, "annotated")).outputText)).toEqual({ + content: [ + { + _meta: { source: "provider" }, + annotations: { audience: ["assistant"] }, + text: "annotated", + type: "text", + }, + ], + }); + await expect(execute(`${baseUrl}/scope`, "scope")).rejects.toThrow( + "credential for Test MCP lacks the access required for lookup", + ); + + expect(methods.get("modern")).toEqual(["server/discover", "tools/call"]); + expect(methods.get("mixed")).toEqual(["server/discover", "tools/call"]); + expect(methods.get("annotated")).toEqual(["server/discover", "tools/call"]); + expect(methods.get("legacy")).toEqual([ + "server/discover", + "initialize", + "notifications/initialized", + "tools/call", + ]); + expect(methods.get("scope")).toEqual(["server/discover", "tools/call"]); + expect(protocolHeaders).toEqual([ + "2026-07-28", + expect.stringMatching(/^2025-/), + "2026-07-28", + "2026-07-28", + "2026-07-28", + ]); + } finally { + await server.stop(true); + } +}); + +test("terminates an established MCP session after cancellation", async () => { + const callStarted = Promise.withResolvers(); + const cancelled = Promise.withResolvers(); + const sessionDeleted = Promise.withResolvers(); + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + if (request.method === "GET") { + return new Response(null, { status: 405 }); + } + + if (request.method === "DELETE") { + sessionDeleted.resolve(request.headers.get("mcp-session-id")); + return new Response(null, { status: 200 }); + } + + const message = (await request.json()) as { + id?: unknown; + method?: string; + params?: { protocolVersion?: string }; + }; + + if (message.method === "server/discover") { + return new Response("not found", { status: 404 }); + } + + if (message.method === "initialize") { + return Response.json( + { + id: message.id, + jsonrpc: "2.0", + result: { + capabilities: { tools: {} }, + protocolVersion: message.params?.protocolVersion, + serverInfo: { name: "test-mcp", version: "1" }, + }, + }, + { headers: { "mcp-session-id": "cancelled-session" } }, + ); + } + + if (message.method === "notifications/initialized") { + return new Response(null, { status: 202 }); + } + + if (message.method === "notifications/cancelled") { + cancelled.resolve(); + return new Response(null, { status: 202 }); + } + + if (message.method === "tools/call") { + callStarted.resolve(); + if (!request.signal.aborted) { + await Promise.race([ + cancelled.promise, + new Promise((resolve) => + request.signal.addEventListener("abort", () => resolve(), { once: true }), + ), + ]); + } + return Response.json({ + id: message.id, + jsonrpc: "2.0", + result: { content: [{ text: "cancelled", type: "text" }] }, + }); + } + + return new Response("unexpected request", { status: 400 }); + }, + }); + + try { + const controller = new AbortController(); + const execution = execute( + `http://${server.hostname}:${server.port}/cancel`, + "cancel", + controller.signal, + ); + await callStarted.promise; + controller.abort(new Error("cancel requested")); + + await expect(execution).rejects.toThrow(); + expect(await sessionDeleted.promise).toBe("cancelled-session"); + } finally { + await server.stop(true); + } +}); diff --git a/tests/skill-materialization.test.ts b/tests/skill-materialization.test.ts index b514432..0346844 100644 --- a/tests/skill-materialization.test.ts +++ b/tests/skill-materialization.test.ts @@ -13,6 +13,7 @@ import { } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { zipSync as createZipArchive } from "fflate"; import type { AgentDriverMaterializedSkill } from "../src/host-ports"; import { createBufferedSinkLogger } from "../src/observability"; @@ -22,8 +23,6 @@ import { exposeNativeSkillAliases, materializeResolvedSkills, } from "../src/runtimes/skill-materialization"; -import { createZipArchive } from "../src/skill-package"; -import type { SkillPackageEntry } from "../src/skill-package"; import { bootPayload } from "./driver-runtime-boundary-fixtures"; const textEncoder = new TextEncoder(); @@ -73,15 +72,8 @@ function createTestLogger() { }); } -function createMarkdownSkillEntries(markdown: string): SkillPackageEntry[] { - return [ - { - body: textEncoder.encode(markdown), - entryKind: "file", - isExecutable: false, - path: "SKILL.md", - }, - ]; +function createMarkdownSkillEntries(markdown: string) { + return { "SKILL.md": textEncoder.encode(markdown) }; } describe("skill materialization", () => { @@ -146,14 +138,9 @@ Check the diff.`), test("fails malformed packages before reporting materialization success", async () => { const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); const logger = createTestLogger(); - const archive = createZipArchive([ - { - body: textEncoder.encode("missing skill markdown"), - entryKind: "file", - isExecutable: false, - path: "references/README.md", - } satisfies SkillPackageEntry, - ]); + const archive = createZipArchive({ + "references/README.md": textEncoder.encode("missing skill markdown"), + }); const skill = createSkill(root, archive); try { From 19bbfcf6e0e7907fefd2d6bd33a7eff281989fbc Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Thu, 27 Aug 2026 15:07:17 +0000 Subject: [PATCH 02/20] refactor(runtime): harden lifecycle and resource ownership --- src/bin/driver-process.ts | 19 +- src/core/agent-backend-lifecycle.ts | 272 ++-- src/core/agent-driver-backend.ts | 2 +- src/core/agent-driver-kernel.ts | 110 +- src/core/driver-command-dispatcher.ts | 103 +- src/core/driver-permission-broker.ts | 23 +- src/core/driver-runtime-io.ts | 1 + src/core/driver-runtime-state.ts | 25 +- src/host-ports/index.ts | 24 +- src/index.ts | 10 +- src/infrastructure/logging/driver-logger.ts | 5 +- .../runtime/driver-instance-socket.ts | 315 +++- src/models/index.ts | 1 - src/observability/driver-debug-paths.ts | 4 +- src/protocol/boot/index.ts | 39 +- .../events/runtime-event-validation.ts | 51 +- src/protocol/events/runtime-events.ts | 54 +- src/protocol/paths/index.ts | 7 +- src/protocol/start.ts | 2 +- src/runtimes/acp/acp-agent-process.ts | 44 +- .../acp/acp-assistant-transcript-state.ts | 309 ++-- .../acp/acp-client-request-handler.ts | 51 +- src/runtimes/acp/acp-driver-backend.ts | 187 ++- src/runtimes/acp/acp-file-system.ts | 144 +- src/runtimes/acp/acp-lifecycle-events.ts | 6 +- src/runtimes/acp/acp-path-scope.ts | 287 +++- src/runtimes/acp/acp-session-events.ts | 2 +- src/runtimes/acp/acp-terminal-manager.ts | 49 +- src/runtimes/acp/acp-tool-events.ts | 153 +- src/runtimes/acp/acp-turn-controller.ts | 152 +- src/runtimes/atomic-file.ts | 293 ++++ src/runtimes/child-process-env.ts | 22 +- src/runtimes/child-process.ts | 16 +- .../claude/agent-sdk-driver-backend.ts | 9 +- .../claude/agent-sdk-message-translator.ts | 30 +- src/runtimes/claude/agent-sdk-prewarm.ts | 183 ++- src/runtimes/claude/agent-sdk-tasks.ts | 29 +- src/runtimes/driver-event-admission.ts | 25 +- src/runtimes/driver-event-publisher.ts | 32 +- src/runtimes/mcp/remote-http-mcp-executor.ts | 105 +- src/runtimes/mcp/server-key.ts | 5 +- src/runtimes/openai/app-server-client.ts | 314 +++- .../openai/app-server-driver-backend.ts | 169 ++- .../openai/app-server-event-bridge.ts | 5 +- .../openai/app-server-event-mapping.ts | 2 +- .../openai/app-server-protocol-types.ts | 7 - src/runtimes/openai/auth-state.ts | 55 +- src/runtimes/provider-registry.ts | 71 +- src/runtimes/runtime-turn-transcript.ts | 4 - src/runtimes/skill-bootstrap.ts | 569 ++++++- src/runtimes/skill-materialization.ts | 1328 ++++++++++++++--- src/stores/cma-store.ts | 4 + src/stores/memory/cma-memory-store.ts | 34 +- src/surfaces/cma-http/session-events.ts | 3 +- src/surfaces/cma-sdk/client.ts | 346 ++++- src/surfaces/cma-sdk/index.ts | 2 + src/surfaces/cma-sdk/sse-bytes-decoder.ts | 18 +- src/surfaces/cma-sdk/types.ts | 45 +- tests/acp-agent-process.test.ts | 196 ++- tests/acp-client-request-handler.test.ts | 175 ++- tests/acp-configuration.test.ts | 21 +- tests/acp-driver-backend.test.ts | 473 +++++- tests/acp-event-translator-lifecycle.test.ts | 255 +++- ...cp-event-translator-session-update.test.ts | 10 +- tests/acp-file-system.test.ts | 259 +++- tests/acp-process-supervision.test.ts | 4 +- tests/acp-terminal-manager.test.ts | 97 +- tests/agent-backend-lifecycle.test.ts | 175 +++ tests/agent-driver-kernel-async-queue.test.ts | 126 ++ tests/agent-driver-kernel-commands.test.ts | 248 +-- tests/agent-driver-kernel-lifecycle.test.ts | 28 +- tests/child-process-env.test.ts | 67 +- tests/claude-agent-sdk-driver-backend.test.ts | 87 ++ tests/claude-agent-sdk-durability.test.ts | 11 +- tests/claude-agent-sdk-process.test.ts | 21 + ...ent-sdk-provider-fixtures-terminal.test.ts | 65 +- tests/cma-http-request.test.ts | 52 +- tests/cma-http-sse.test.ts | 38 +- tests/cma-sdk.test.ts | 121 +- tests/cma-store-claims.test.ts | 25 +- tests/cma-store-events.test.ts | 49 +- tests/driver-artifact-contract.test.ts | 46 +- tests/driver-artifact-live.test.ts | 5 +- .../driver-event-publisher-admission.test.ts | 127 +- ...river-event-publisher-backpressure.test.ts | 4 +- tests/driver-golden-fixtures.test.ts | 12 + tests/driver-instance-socket-process.test.ts | 143 +- tests/driver-instance-socket-socket.test.ts | 337 +++++ tests/driver-logger.test.ts | 21 +- tests/driver-permission-broker.test.ts | 68 + tests/driver-runtime-boundary-fixtures.ts | 35 +- tests/driver-runtime-boundary-state.test.ts | 655 +++++++- .../acp/cases/turn-text-tool-usage.json | 7 - .../cases/root-with-parallel-child-turns.json | 14 +- tests/mcp-server-key.test.ts | 28 + tests/openai-app-server-auth-state.test.ts | 32 +- tests/openai-app-server-client.test.ts | 142 +- ...penai-app-server-provider-fixtures.test.ts | 85 +- tests/openai-app-server-startup.test.ts | 244 ++- tests/opencode-acp-contract.test.ts | 27 +- tests/process-tree-watchdog.test.ts | 31 +- tests/provider-registry.test.ts | 46 +- tests/public-api-consumer.ts | 4 - tests/public-api-exports.snapshot.json | 10 +- tests/remote-http-mcp-executor.test.ts | 10 +- tests/skill-bootstrap.test.ts | 484 ++++++ tests/skill-materialization.test.ts | 912 ++++++++++- 107 files changed, 10094 insertions(+), 2214 deletions(-) delete mode 100644 src/models/index.ts create mode 100644 src/runtimes/atomic-file.ts create mode 100644 tests/agent-backend-lifecycle.test.ts create mode 100644 tests/mcp-server-key.test.ts create mode 100644 tests/skill-bootstrap.test.ts diff --git a/src/bin/driver-process.ts b/src/bin/driver-process.ts index 8c914d0..89dc156 100644 --- a/src/bin/driver-process.ts +++ b/src/bin/driver-process.ts @@ -29,7 +29,7 @@ import type { RunId } from "../protocol/id"; import { createDriverStartInputFromBootPayload } from "../protocol/start"; import type { DriverStartInput } from "../protocol/start"; import type { RunError } from "../runtime-command"; -import { executeRemoteHttpMcpCommand } from "../runtimes/mcp/remote-http-mcp-executor"; +import { prepareRemoteHttpMcpCommand } from "../runtimes/mcp/remote-http-mcp-executor"; import { AGENT_DRIVER_PROVIDER_REGISTRY, createAgentDriverProviderCapabilities, @@ -171,7 +171,6 @@ export class DriverProcess { backend, createContext: () => this.createAgentDriverContext(socket, logger), labels: { - deferredStop: "Driver deferred backend shutdown", finalStop: "Driver final backend shutdown", start: "Driver backend startup", stop: "Driver backend shutdown", @@ -522,7 +521,9 @@ export class DriverProcess { } try { - return await this.#permissionBroker.request(socket, input, signal); + return await this.#permissionBroker.request(socket, input, signal, () => + this.#runtimeState.ownsRun(generation), + ); } finally { this.#runtimeState.endApproval(generation); } @@ -531,19 +532,15 @@ export class DriverProcess { }, ports: { mcp: { - execute: async (command, signal, effect) => { - if (effect === undefined) { - throw new Error("Driver external tool effect ledger is not configured."); - } - - return executeRemoteHttpMcpCommand(this.#startInput, command, signal, effect); - }, + prepare: (command, signal) => + prepareRemoteHttpMcpCommand(this.#startInput, command, signal), }, hostIntegration: { snapshot: async () => this.#hostSnapshot, }, skill: { - materialize: async (execution) => materializeResolvedSkills(execution, logger), + materialize: async (execution, signal) => + materializeResolvedSkills(execution, logger, signal), }, }, }); diff --git a/src/core/agent-backend-lifecycle.ts b/src/core/agent-backend-lifecycle.ts index 1422f05..6cc1dc2 100644 --- a/src/core/agent-backend-lifecycle.ts +++ b/src/core/agent-backend-lifecycle.ts @@ -1,11 +1,10 @@ -import { promiseWithTimeout } from "../utils/async"; +import { promiseWithTimeout, settlePromiseWithTimeout } from "../utils/async"; import type { AgentDriverBackend, AgentDriverContext } from "./agent-driver-backend"; export interface AgentBackendLifecycleOptions { readonly backend: AgentDriverBackend; readonly createContext: () => AgentDriverContext; readonly labels: { - readonly deferredStop: string; readonly finalStop: string; readonly start: string; readonly stop: string; @@ -28,6 +27,20 @@ export interface AgentBackendLifecycleOptions { readonly stopTimeoutMs: number; } +type CleanupOwner = { + deadline: number; + deferred: DeferredCleanup | null; + task: Promise; +}; + +type DeferredCleanup = + | { after: Promise; kind: "final_stop" } + | { generation: number; kind: "late_stop"; task: Promise }; + +type StopOperation = { + settled: Promise; +}; + export class AgentBackendLifecycle { readonly #backend: AgentDriverBackend; readonly #createContext: () => AgentDriverContext; @@ -39,12 +52,13 @@ export class AgentBackendLifecycle { readonly #shutdownSignal: AbortSignal; readonly #startTimeoutMs: number; readonly #stopTimeoutMs: number; - #finalStopTask: Promise | null = null; + #cleanupOwner: CleanupOwner | null = null; + #inFlightStop: StopOperation | null = null; #startController: AbortController | null = null; + #startupStopTask: Promise | null = null; #startTask: Promise | null = null; - #stopController: AbortController | null = null; - #stopNeedsReplay = false; - #stopTask: Promise | null = null; + #stopGeneration = 0; + #stopped = false; constructor(options: AgentBackendLifecycleOptions) { this.#backend = options.backend; @@ -92,135 +106,177 @@ export class AgentBackendLifecycle { } async shutdown(reason: string): Promise { - const startTask = this.#startTask; - const finalStopTask = this.#finalStopTask; - - if (startTask !== null) { - this.#startController?.abort(new Error(reason)); - this.#stopNeedsReplay = true; + if (this.#stopped) { + return; } - if (this.#stopTask === null) { - if (startTask === null) { - this.#stopNeedsReplay = false; - } - this.#stop(reason); - } + await (this.#cleanupOwner ?? this.#createCleanupOwner(reason)).task; + } - const tasks: Promise[] = []; - if (this.#stopTask !== null) { - tasks.push(this.#stopTask); - } - if (startTask !== null) { - tasks.push(startTask.catch(() => {})); - } - if (finalStopTask !== null) { - tasks.push(finalStopTask); + #clearStart(task: Promise): void { + if (this.#startTask === task) { + this.#startController = null; + this.#startTask = null; } + } - try { - await promiseWithTimeout(Promise.all(tasks), { - label: this.#labels.stop, - timeoutMs: this.#stopTimeoutMs, - }); + #createCleanupOwner(reason: string): CleanupOwner { + const startTask = this.#startTask; + this.#startController?.abort(new Error(reason)); - if (this.#stopNeedsReplay) { - this.#stopNeedsReplay = false; - await promiseWithTimeout(this.#stop(reason), { - label: this.#labels.finalStop, - timeoutMs: this.#stopTimeoutMs, - }); - } + const owner: CleanupOwner = { + deadline: Date.now() + this.#stopTimeoutMs, + deferred: null, + task: Promise.resolve(), + }; + owner.task = this.#runCleanup(owner, reason, startTask).then( + () => { + this.#stopped = true; + if (this.#cleanupOwner === owner) { + this.#cleanupOwner = null; + } + }, + (error: unknown) => { + if (this.#cleanupOwner === owner) { + this.#cleanupOwner = null; + } + if (owner.deferred !== null) { + this.#watchDeferredCleanup(owner.deferred, reason); + } + throw error; + }, + ); + this.#cleanupOwner = owner; + return owner; + } - this.#clear(); - } catch (error) { - this.#stopController?.abort(error); - this.#stopController = null; - this.#stopTask = null; - if (this.#finalStopTask === finalStopTask) { - this.#finalStopTask = null; - } - if (startTask !== null && this.#stopNeedsReplay) { - this.#scheduleFinalStop(startTask, reason); - } - throw error; + async #runCleanup( + owner: CleanupOwner, + reason: string, + startTask: Promise | null, + ): Promise { + if (startTask === null) { + await this.#stop(owner, reason, this.#labels.stop, true); + return; + } + + if (this.#startupStopTask !== startTask) { + this.#startupStopTask = startTask; + await this.#stop(owner, reason, this.#labels.stop, false).catch(() => {}); } + + const startResult = await settlePromiseWithTimeout(startTask, { + label: this.#labels.stop, + timeoutMs: this.#remaining(owner), + }); + if (startResult.status === "timed_out") { + const stopSettled = this.#inFlightStop?.settled ?? Promise.resolve(); + owner.deferred = { + after: Promise.allSettled([startTask, stopSettled]).then(() => {}), + kind: "final_stop", + }; + throw startResult.error; + } + + await this.#stop(owner, reason, this.#labels.finalStop, true); } - #clear(): void { - this.#finalStopTask = null; - this.#startController = null; - this.#stopController = null; - this.#stopTask = null; + #remaining(owner: CleanupOwner): number { + return Math.max(0, owner.deadline - Date.now()); } - #clearStart(task: Promise): void { - if (this.#startTask === task) { - this.#startController = null; - this.#startTask = null; + async #stop(owner: CleanupOwner, reason: string, label: string, final: boolean): Promise { + const previous = this.#inFlightStop; + if (previous !== null) { + const previousResult = await settlePromiseWithTimeout(previous.settled, { + label, + timeoutMs: this.#remaining(owner), + }); + if (previousResult.status !== "completed") { + if (final && previousResult.status === "timed_out") { + owner.deferred = { after: previous.settled, kind: "final_stop" }; + } + throw previousResult.error; + } } - } - #stop(reason: string): Promise { const controller = new AbortController(); + const generation = (this.#stopGeneration += 1); const task = Promise.resolve().then(() => this.#runStop(this.#backend, this.#createContext(), reason, controller.signal), ); - this.#stopController = controller; - this.#stopTask = task; - void task.then(undefined, () => { - if (this.#stopTask === task) { - this.#stopController = null; - this.#stopTask = null; - } + const operation: StopOperation = { + settled: Promise.resolve(), + }; + operation.settled = task.then( + () => this.#clearStop(operation), + () => this.#clearStop(operation), + ); + this.#inFlightStop = operation; + + const result = await settlePromiseWithTimeout(task, { + label, + timeoutMs: this.#remaining(owner), }); - return task; + if (result.status === "completed") { + return; + } + if (result.status === "timed_out") { + controller.abort(result.error); + if (final) { + owner.deferred = { generation, kind: "late_stop", task }; + } + } + throw result.error; + } + + #clearStop(operation: StopOperation): void { + if (this.#inFlightStop === operation) { + this.#inFlightStop = null; + } } - #scheduleFinalStop(startTask: Promise, reason: string): void { - if (this.#finalStopTask !== null) { + #watchDeferredCleanup(deferred: DeferredCleanup, reason: string): void { + if (deferred.kind === "final_stop") { + void deferred.after.then(() => this.#runDeferredCleanup(reason)); return; } - let stopTask: Promise | null = null; - let task!: Promise; - task = startTask - .catch(() => {}) - .then(async () => { - if (this.#finalStopTask !== task || !this.#stopNeedsReplay) { + void deferred.task.then( + () => this.#acceptLateStop(deferred.generation), + (error: unknown) => this.#onDeferredStopError?.(error), + ); + } + + async #runDeferredCleanup(reason: string): Promise { + try { + await this.shutdown(reason); + this.#onDeferredStopComplete?.(); + } catch (error) { + this.#onDeferredStopError?.(error); + } + } + + async #acceptLateStop(generation: number): Promise { + const owner = this.#cleanupOwner; + if (owner !== null) { + try { + await owner.task; + this.#onDeferredStopComplete?.(); + return; + } catch (error) { + if (generation !== this.#stopGeneration || this.#inFlightStop !== null) { + this.#onDeferredStopError?.(error); return; } + } + } - this.#stopNeedsReplay = false; - stopTask = this.#stop(reason); - await promiseWithTimeout(stopTask, { - label: this.#labels.deferredStop, - timeoutMs: this.#stopTimeoutMs, - }); + if (generation !== this.#stopGeneration) { + return; + } - if (this.#finalStopTask === task && this.#stopTask === stopTask) { - this.#clear(); - this.#onDeferredStopComplete?.(); - } - }); - this.#finalStopTask = task; - void task.then( - () => { - if (this.#finalStopTask === task) { - this.#finalStopTask = null; - } - }, - (error: unknown) => { - if (this.#finalStopTask === task) { - this.#finalStopTask = null; - } - if (stopTask !== null && this.#stopTask === stopTask) { - this.#stopController?.abort(error); - this.#stopController = null; - this.#stopTask = null; - } - this.#onDeferredStopError?.(error); - }, - ); + this.#stopped = true; + this.#onDeferredStopComplete?.(); } } diff --git a/src/core/agent-driver-backend.ts b/src/core/agent-driver-backend.ts index 70a08f8..77bbe83 100644 --- a/src/core/agent-driver-backend.ts +++ b/src/core/agent-driver-backend.ts @@ -109,7 +109,7 @@ function createDefaultHostPorts(input: AgentDriverContextInput): AgentDriverHost snapshot: async () => null, }, mcp: { - execute: async () => { + prepare: async () => { throw new Error("Driver MCP host port is not configured."); }, }, diff --git a/src/core/agent-driver-kernel.ts b/src/core/agent-driver-kernel.ts index d09880e..b8dd6b1 100644 --- a/src/core/agent-driver-kernel.ts +++ b/src/core/agent-driver-kernel.ts @@ -44,6 +44,10 @@ export interface AgentDriverKernelOptions { } type KernelCommandResult = RuntimeCommandResult | void; +type RunEventTerminal = { + readonly runId: RunId; + readonly status: "cancelled" | "completed" | "failed"; +}; const KERNEL_SHUTDOWN_TIMEOUT_MS = 5_000; const KERNEL_START_TIMEOUT_MS = 60_000; @@ -102,13 +106,11 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo readonly #shutdownController = new AbortController(); #activeRunId: RunId | null = null; #backendLifecycle: AgentBackendLifecycle | null = null; + #eventFinalizationTask: Promise | null = null; #pushedEventSeq = 0; #runTask: Promise | null = null; #runTaskSettled = false; - #runEventTerminal: { - readonly runId: RunId; - readonly status: "cancelled" | "completed" | "failed"; - } | null = null; + #runEventTerminal: RunEventTerminal | null = null; #shutdownComplete = false; #shutdownRunFailure: { error: RunError; runId: RunId | null } | null = null; #runTerminal: "cancelled" | "completed" | "failed" | null = null; @@ -125,6 +127,9 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } beginRun(runId: RunId): void { + if (this.#activeRunId !== null) { + throw new Error(`Driver run ${this.#activeRunId} is already active.`); + } this.#activeRunId = runId; this.#runEventTerminal = null; this.#runTerminal = null; @@ -267,7 +272,22 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } async pushEvents(input: { events: DriverEventInput[] }): Promise { const events = structuredClone(input.events); - for (const event of events) { + let terminal: RunEventTerminal | null = null; + let terminalIndex = -1; + + for (const [index, event] of events.entries()) { + const runId = event.runId === undefined ? this.#activeRunId : event.runId; + + if (runId !== null) { + if (runId !== this.#activeRunId) { + throw new Error("Driver event must target the active run."); + } + + if (this.#runTerminal !== null) { + throw new Error("Driver event cannot target a terminated run."); + } + } + const status = event.kind === "run.cancelled" ? "cancelled" @@ -276,13 +296,38 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo : event.kind === "run.failed" ? "failed" : null; - const runId = event.runId ?? this.#activeRunId; - if (status !== null && runId !== null) { - this.#runEventTerminal = { runId, status }; - this.#runTerminal = status; + + if (status === null) { + continue; } + + if (runId === null) { + throw new Error("Driver run terminal must target the active run."); + } + + if (event.delivery === "best_effort") { + throw new Error("Driver run terminal must be lossless."); + } + + if (terminal !== null) { + throw new Error("Driver event batch cannot contain multiple run terminals."); + } + + terminal = { runId, status }; + terminalIndex = index; } + + if (terminalIndex >= 0 && terminalIndex !== events.length - 1) { + throw new Error("Driver run terminal must be the final event in its batch."); + } + this.#events.pushMany(events); + + if (terminal !== null) { + this.#runEventTerminal = terminal; + this.#runTerminal = terminal.status; + } + const accepted = events.map((event) => { this.#pushedEventSeq += 1; @@ -299,6 +344,10 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo return this.#runEventTerminal?.runId === runId ? this.#runEventTerminal.status : null; } + selectedRunEventTerminal(runId: RunId): "cancelled" | "completed" | "failed" | null { + return this.runEventTerminal(runId); + } + async start(input: AgentDriverKernelStartInput): Promise { if (this.#runtimeState.status() !== "created") { throw new Error("Driver kernel has already started."); @@ -325,13 +374,14 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo backend, createContext: () => this.#createContext(input), labels: { - deferredStop: "Driver kernel deferred backend shutdown", finalStop: "Driver kernel final backend shutdown", start: "Driver kernel backend startup", stop: "Driver kernel backend shutdown", }, onDeferredStopComplete: () => { - void this.#closeEventsAfterPermissions(); + void this.#completeShutdown().catch((error: unknown) => { + this.#logger.error("driver.kernel.deferred_finalization.failed", error, {}); + }); }, onDeferredStopError: (error) => { this.#logger.error("driver.kernel.deferred_shutdown.failed", error, {}); @@ -391,9 +441,7 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo this.#rejectPendingCommands( this.#terminalCause?.error ?? new Error("Driver kernel stopped."), ); - if (this.#shutdownComplete) { - await this.#finalizeEvents(); - } + await this.#finalizeShutdown(); }); } catch (error) { this.#rememberTerminalCause(error); @@ -491,7 +539,9 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } try { - return await this.#permissionBroker.request(this, input, signal); + return await this.#permissionBroker.request(this, input, signal, () => + this.#runtimeState.ownsRun(generation), + ); } finally { this.#runtimeState.endApproval(generation); } @@ -595,6 +645,26 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo await this.#closeEventsAfterPermissions(); } + async #completeShutdown(): Promise { + if (this.#runtimeState.status() === "stopping") { + this.#runtimeState.enter("stopped"); + } + this.#backendLifecycle = null; + this.#shutdownComplete = true; + await this.#finalizeShutdown(); + } + + async #finalizeShutdown(): Promise { + if (!this.#shutdownComplete || (this.#runTask !== null && !this.#runTaskSettled)) { + return; + } + + await (this.#eventFinalizationTask ??= this.#finalizeEvents().catch((error: unknown) => { + this.#eventFinalizationTask = null; + throw error; + })); + } + #shutdown(reason: string): Promise { return (this.#shutdownTask ??= this.#runShutdown(reason).catch((error: unknown) => { this.#shutdownTask = null; @@ -604,6 +674,7 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo async #runShutdown(reason: string): Promise { if (this.#runtimeState.status() === "stopped") { + await this.#finalizeShutdown(); return; } @@ -633,14 +704,7 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo throw failure.reason; } - if (this.#runtimeState.status() === "stopping") { - this.#runtimeState.enter("stopped"); - } - this.#backendLifecycle = null; - this.#shutdownComplete = true; - if (this.#runTask === null || this.#runTaskSettled) { - await this.#finalizeEvents(); - } + await this.#completeShutdown(); } catch (error) { if (this.#runtimeState.status() === "stopping") { this.#runtimeState.enter("failed"); diff --git a/src/core/driver-command-dispatcher.ts b/src/core/driver-command-dispatcher.ts index 132b627..dbabe79 100644 --- a/src/core/driver-command-dispatcher.ts +++ b/src/core/driver-command-dispatcher.ts @@ -40,6 +40,7 @@ interface DriverCommandDispatcherOptions { const COMMAND_POLL_INTERVAL_MS = 250; export const ACTIVE_TURN_CANCEL_GRACE_MS = 2_000; const ACTIVE_INPUT_SETTLE_GRACE_MS = ACTIVE_TURN_CANCEL_GRACE_MS + DRIVER_EVENT_DELIVERY_TIMEOUT_MS; +const EXTERNAL_TOOL_EFFECT_COMPLETE_ATTEMPTS = 2; const EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS = 2_000; const MAX_ACTIVE_MCP_COMMANDS = 32; @@ -567,8 +568,8 @@ export class DriverCommandDispatcher { command: Extract, controller: AbortController, ): Promise { - let effectClaimed = false; - let effectCompleted = false; + let effectMayBeClaimed = false; + let durableResult: McpExecuteCommandResult | null = null; const effectLedger = runtimeContext.ports.eventSink; if ( @@ -592,42 +593,55 @@ export class DriverCommandDispatcher { }, }, ]); - const effect = await effectLedger.claimExternalToolEffect( + controller.signal.throwIfAborted(); + await using prepared = await runtimeContext.ports.mcp.prepare(command, controller.signal); + controller.signal.throwIfAborted(); + const pendingClaim = effectLedger.claimExternalToolEffect( { commandId: command.commandId }, controller.signal, ); - if (effect.kind === "unknown") { - throw new ExternalToolEffectResolutionRequiredError(command, effect.effectId); + effectMayBeClaimed = true; + const claim = await pendingClaim; + if (claim.kind === "unknown") { + effectMayBeClaimed = false; + throw new ExternalToolEffectResolutionRequiredError(command, claim.effectId); } let result: McpExecuteCommandResult; - - if (effect.kind === "completed") { - result = effect.result; + if (claim.kind === "completed") { + effectMayBeClaimed = false; + result = claim.result; + durableResult = result; } else { - effectClaimed = true; runtimeContext.logger.info("driver.runtime.mcp.execute.started", { - effectAttempt: effect.attempt, + effectAttempt: claim.attempt, serverId: command.serverId, toolName: command.toolName, }); - const execution = await runtimeContext.ports.mcp.execute( - command, - controller.signal, - effect, - ); + const execution = await prepared.execute(claim); const { providerReceiptJson, ...executionResult } = execution; result = executionResult; - controller.signal.throwIfAborted(); - await effectLedger.completeExternalToolEffect( - { - commandId: command.commandId, - ...(providerReceiptJson === undefined ? {} : { providerReceiptJson }), - result, - }, - controller.signal, - ); - effectCompleted = true; + let completionFailure: { error: unknown } | null = null; + for (let attempt = 0; attempt < EXTERNAL_TOOL_EFFECT_COMPLETE_ATTEMPTS; attempt += 1) { + try { + await effectLedger.completeExternalToolEffect( + { + commandId: command.commandId, + ...(providerReceiptJson === undefined ? {} : { providerReceiptJson }), + result, + }, + AbortSignal.timeout(EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS), + ); + durableResult = result; + completionFailure = null; + break; + } catch (error) { + completionFailure = { error }; + } + } + if (completionFailure !== null) { + throw completionFailure.error; + } } await pushLosslessEvents(socket, [ @@ -653,10 +667,20 @@ export class DriverCommandDispatcher { toolName: command.toolName, }); } catch (error) { - if (effectClaimed && !effectCompleted) { + if (effectMayBeClaimed && durableResult === null) { await this.#markExternalToolEffectUnknown(effectLedger, command.commandId); } + if (durableResult !== null) { + if (!this.#commandDelivery.hasTerminal(command.commandId)) { + await this.#commandDelivery.finish(runtimeContext, command, { + result: durableResult, + status: "completed", + }); + } + throw error; + } + if (this.#commandDelivery.hasTerminal(command.commandId)) { throw error; } @@ -700,10 +724,7 @@ export class DriverCommandDispatcher { throw new Error("Driver external tool effect ledger is not configured."); } - const signal = AbortSignal.any([ - this.#shutdownSignal, - AbortSignal.timeout(EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS), - ]); + const signal = AbortSignal.timeout(EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS); await markUnknown.call(effectLedger, { commandId }, signal); } @@ -772,10 +793,28 @@ export class DriverCommandDispatcher { cancellation: AbortController, runId: RunId, ): Promise { + let terminalSelectedAtCancellation: "cancelled" | "completed" | "failed" | null = null; + const captureTerminalSelection = () => { + terminalSelectedAtCancellation = + socket.selectedRunEventTerminal?.(runId) ?? socket.runEventTerminal?.(runId) ?? null; + }; + cancellation.signal.addEventListener("abort", captureTerminalSelection, { once: true }); + if (cancellation.signal.aborted) { + captureTerminalSelection(); + } + try { cancellation.signal.throwIfAborted(); await this.#backend.handleInput(runtimeContext, command.input, runId, cancellation.signal); - cancellation.signal.throwIfAborted(); + if (cancellation.signal.aborted) { + const deliveredTerminal = socket.runEventTerminal?.(runId) ?? null; + if ( + terminalSelectedAtCancellation === null || + deliveredTerminal !== terminalSelectedAtCancellation + ) { + cancellation.signal.throwIfAborted(); + } + } await this.#commandDelivery.finish(runtimeContext, command, { result: { requestId: command.requestId, @@ -837,6 +876,8 @@ export class DriverCommandDispatcher { this.#rememberRunFailure(commandFailure); await this.#shutdown(socket, commandFailure.code); this.#shutdownCompleted = true; + } finally { + cancellation.signal.removeEventListener("abort", captureTerminalSelection); } } diff --git a/src/core/driver-permission-broker.ts b/src/core/driver-permission-broker.ts index 8dd0a42..bca55c2 100644 --- a/src/core/driver-permission-broker.ts +++ b/src/core/driver-permission-broker.ts @@ -146,11 +146,12 @@ export class DriverPermissionBroker { timeoutMs: this.#eventDeliveryTimeoutMs * 2, }); this.#cancellationTask = task; - void idle.promise.then(() => { + const clear = () => { if (this.#cancellationTask === task) { this.#cancellationTask = null; } - }); + }; + void task.then(clear, clear); return task; } @@ -170,6 +171,7 @@ export class DriverPermissionBroker { socket: DriverRuntimeEventPort, input: DriverPermissionRequest, signal?: AbortSignal, + ownsRun?: () => boolean, ): Promise { if (!this.#interactiveRequests) { this.#logger()?.debug("driver.runtime.permission.request.rejected", { @@ -194,6 +196,7 @@ export class DriverPermissionBroker { } const runId = socket.currentRunId(); + const isCurrentRun = ownsRun ?? (() => socket.currentRunId() === runId); const events: DriverEventInput[] = [ { kind: "permission.requested", @@ -320,6 +323,9 @@ export class DriverPermissionBroker { if (requestedDelivery.status === "timed_out") { lateDelivery = requestedTask; } + if (!isCurrentRun()) { + return "reject_once"; + } throw new PermissionEventDeliveryError( input.requestId, "requested", @@ -327,6 +333,10 @@ export class DriverPermissionBroker { ); } + if (!isCurrentRun()) { + return "reject_once"; + } + this.#logger()?.debug("driver.runtime.permission.request.sent", { requestId: input.requestId, timeoutMs: this.#requestTimeoutMs, @@ -357,6 +367,10 @@ export class DriverPermissionBroker { }); } + if (!isCurrentRun()) { + return "reject_once"; + } + const resolutionTask = pushLosslessEvents( socket, [ @@ -387,6 +401,9 @@ export class DriverPermissionBroker { if (resolutionDelivery.status === "timed_out") { lateDelivery = resolutionTask; } + if (!isCurrentRun()) { + return "reject_once"; + } throw new PermissionEventDeliveryError( input.requestId, "resolved", @@ -400,7 +417,7 @@ export class DriverPermissionBroker { requestId: input.requestId, }); - return decision; + return isCurrentRun() ? decision : "reject_once"; } finally { unregister(); signal?.removeEventListener("abort", cancel); diff --git a/src/core/driver-runtime-io.ts b/src/core/driver-runtime-io.ts index 8df9a42..f698fdd 100644 --- a/src/core/driver-runtime-io.ts +++ b/src/core/driver-runtime-io.ts @@ -23,6 +23,7 @@ export interface DriverRuntimeEventPort { signal?: AbortSignal; }): Promise; runEventTerminal?(runId: RunId): "cancelled" | "completed" | "failed" | null; + selectedRunEventTerminal?(runId: RunId): "cancelled" | "completed" | "failed" | null; } export const DRIVER_EVENT_DELIVERY_TIMEOUT_MS = 10_000; diff --git a/src/core/driver-runtime-state.ts b/src/core/driver-runtime-state.ts index d6abbf1..5cf0407 100644 --- a/src/core/driver-runtime-state.ts +++ b/src/core/driver-runtime-state.ts @@ -1,15 +1,12 @@ -export const DRIVER_RUNTIME_STATUSES = [ - "created", - "starting", - "ready", - "running", - "needs_approval", - "stopping", - "stopped", - "failed", -] as const; - -export type DriverRuntimeStatus = (typeof DRIVER_RUNTIME_STATUSES)[number]; +export type DriverRuntimeStatus = + | "created" + | "starting" + | "ready" + | "running" + | "needs_approval" + | "stopping" + | "stopped" + | "failed"; export const DRIVER_RUNTIME_TRANSITIONS: Readonly< Record @@ -60,6 +57,10 @@ export class DriverRuntimeStateMachine { } } + ownsRun(generation: number): boolean { + return this.#activeRunGeneration === generation; + } + beginApproval(): number | null { const generation = this.#activeRunGeneration; diff --git a/src/host-ports/index.ts b/src/host-ports/index.ts index 8fe2cef..079291e 100644 --- a/src/host-ports/index.ts +++ b/src/host-ports/index.ts @@ -14,15 +14,6 @@ import type { RuntimeCommandResult, } from "../runtime-command"; -export type AgentDriverHostPortName = - | "command_source" - | "event_sink" - | "permission" - | "mcp" - | "skill" - | "file" - | "host_integration"; - export interface AgentDriverCommandSource { nextCommand(signal: AbortSignal): Promise; } @@ -81,12 +72,12 @@ export interface DriverPermissionRequest { toolKind: string | null; } +export interface AgentDriverMcpExecution extends AsyncDisposable { + execute(effect: McpExternalToolEffectExecution): Promise; +} + export interface AgentDriverMcpPort { - execute( - command: McpExecuteCommand, - signal: AbortSignal, - effect?: McpExternalToolEffectExecution, - ): Promise; + prepare(command: McpExecuteCommand, signal: AbortSignal): Promise; } export interface AgentDriverMaterializedSkill { @@ -98,7 +89,10 @@ export interface AgentDriverMaterializedSkill { } export interface AgentDriverSkillPort { - materialize(execution: DriverExecutionInput): Promise; + materialize( + execution: DriverExecutionInput, + signal: AbortSignal, + ): Promise; } export interface AgentDriverFilePort { diff --git a/src/index.ts b/src/index.ts index 1f43909..e9bac88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,6 +47,8 @@ export type { CmaSdkClient, CmaSdkClientOptions, CmaSdkFetch, + CmaSdkRequestOptions, + CmaSdkStreamOptions, CmaSessionEventDispatchRecord, } from "./surfaces/cma-sdk"; export { createCmaMemoryStore } from "./stores/memory"; @@ -79,7 +81,6 @@ export type { CmaStoreResourceKind, } from "./stores/cma-store"; export type { - AgentDriverHostPortName, AgentDriverHostPorts, AgentDriverCommandSource, AgentDriverEventSink, @@ -99,7 +100,6 @@ export type { AgentDriverContextPortOverrides, } from "./core/agent-driver-backend"; export { createAgentDriverContext } from "./core/agent-driver-backend"; -export { OPENAI_DEFAULT_MODEL_ID } from "./models"; export { isSupportedDriverRuntime, isSupportedDriverRuntimeTransport, @@ -163,9 +163,5 @@ export type { export { AGENT_DRIVER_PROVIDER_REGISTRY, createAgentDriverProviderCapabilities, - createAgentDriverProviderRegistry, -} from "./runtimes/provider-registry"; -export type { - AgentDriverProviderDescriptor, - AgentDriverProviderRegistry, } from "./runtimes/provider-registry"; +export type { AgentDriverProviderDescriptor } from "./runtimes/provider-registry"; diff --git a/src/infrastructure/logging/driver-logger.ts b/src/infrastructure/logging/driver-logger.ts index 3093f04..2615fd4 100644 --- a/src/infrastructure/logging/driver-logger.ts +++ b/src/infrastructure/logging/driver-logger.ts @@ -109,6 +109,7 @@ export function createDriverLogger( payload: DriverBootPayload, socket: DriverInstanceSocket, ): DriverLoggerHandle { + const sandboxId = payload.execution.session.context.sandboxId; let nextSeq = 0; let openUplink: () => void = () => undefined; // The API rejects pushLogs until the hello handshake commits. Hold every @@ -121,7 +122,7 @@ export function createDriverLogger( const logger = createBufferedSinkLogger({ context: { - sandboxId: payload.sandboxId, + sandboxId, }, flushIntervalMs: 200, level: "trace", @@ -159,7 +160,7 @@ export async function runWithDriverLogContext( return runWithLogContextAsync( createTraceLogContext({ context: { - sandboxId: payload.sandboxId, + sandboxId: payload.execution.session.context.sandboxId, }, service: "driver", traceparent: payload.traceparent, diff --git a/src/infrastructure/runtime/driver-instance-socket.ts b/src/infrastructure/runtime/driver-instance-socket.ts index ac12ac4..a5b4f98 100644 --- a/src/infrastructure/runtime/driver-instance-socket.ts +++ b/src/infrastructure/runtime/driver-instance-socket.ts @@ -5,7 +5,7 @@ import { RPCLink } from "@orpc/client/websocket"; import { assertDriverEventReceiptPrefix } from "../../core/driver-runtime-io"; import type { DriverBootPayload } from "../../protocol/boot"; -import type { DriverEventInput } from "../../protocol/events"; +import type { DriverEventEnvelope, DriverEventInput } from "../../protocol/events"; import type { RunId } from "../../protocol/id"; import type { DriverFailureInput, @@ -28,6 +28,7 @@ import type { RuntimeCommandResult, } from "../../runtime-command"; import { parseRuntimeCommand } from "../../runtime-command"; +import { raceWithAbort } from "../../utils/async"; import { dialDriverControlSocket } from "./driver-control-dial"; import type { DriverWireSocket } from "./driver-control-dial"; import { toDriverEventEnvelopes } from "./driver-event-envelope"; @@ -41,29 +42,69 @@ interface DriverInstanceSocketHandlers { type RunTerminalDelivery = | { delivered: boolean; + readonly runGeneration: number; status: "completed"; task?: Promise; } | { delivered: boolean; error: DriverFailureInput["error"]; + readonly runGeneration: number; status: "failed"; task?: Promise; }; +type RunEventTerminalStatus = "cancelled" | "completed" | "failed"; + +interface RunEventTerminalSelection { + delivered: boolean; + readonly event: Pick; + readonly runId: RunId; + readonly status: RunEventTerminalStatus; + task?: Promise; +} + +interface PreparedEventPush { + readonly client: DriverRuntimeClient; + readonly delivery: DriverEventEnvelope["event"]["delivery"] | undefined; + readonly events: DriverEventEnvelope[]; + readonly generation: number; + readonly hasRunScopedEvent: boolean; + readonly maxBatchSize: number; + readonly runGeneration: number; + readonly selection: RunEventTerminalSelection | null; + readonly signal: AbortSignal | undefined; +} + const DRIVER_RPC_TIMEOUT_MS = 10_000; +const MAX_WEBSOCKET_CLOSE_REASON_BYTES = 123; + +function toWebSocketCloseReason(reason: string): string { + let bytes = 0; + let result = ""; + + for (const character of reason) { + const characterBytes = Buffer.byteLength(character, "utf8"); + if (bytes + characterBytes > MAX_WEBSOCKET_CLOSE_REASON_BYTES) { + break; + } + bytes += characterBytes; + result += character; + } + + return result; +} export class DriverInstanceSocket { #activeRunId: RunId | null = null; #client: DriverRuntimeClient | null = null; #connectionGeneration = 0; #connectAbortController: AbortController | null = null; + #deliveryTail: Promise = Promise.resolve(); #eventBatchMaxSize: number | null = null; #rpcAbortController = new AbortController(); - #runEventTerminal: { - readonly runId: RunId; - readonly status: "cancelled" | "completed" | "failed"; - } | null = null; + #runGeneration = 0; + #runEventTerminal: RunEventTerminalSelection | null = null; #runTerminal: RunTerminalDelivery | null = null; private readonly handlers: DriverInstanceSocketHandlers; private readonly payload: DriverBootPayload; @@ -132,7 +173,7 @@ export class DriverInstanceSocket { this.#client = null; this.#eventBatchMaxSize = null; const socket = this.#socket; - socket?.close(code, reason); + socket?.close(code, toWebSocketCloseReason(reason)); if (this.#socket === socket) { this.#socket = null; @@ -140,13 +181,21 @@ export class DriverInstanceSocket { } beginRun(runId: RunId): void { + if (this.#activeRunId !== null) { + throw new Error("Cannot begin a run while another run is active."); + } + if (this.#runTerminal !== null) { + throw new Error("Cannot begin a run after a control terminal has been selected."); + } + + this.#runGeneration += 1; this.#activeRunId = runId; this.#runEventTerminal = null; - this.#runTerminal = null; } endRun(runId: RunId): void { if (this.#activeRunId === runId) { + this.#runGeneration += 1; this.#activeRunId = null; } } @@ -156,7 +205,13 @@ export class DriverInstanceSocket { } runEventTerminal(runId: RunId): "cancelled" | "completed" | "failed" | null { - return this.#runEventTerminal?.runId === runId ? this.#runEventTerminal.status : null; + const terminal = this.#runEventTerminal; + return terminal?.runId === runId && terminal.delivered ? terminal.status : null; + } + + selectedRunEventTerminal(runId: RunId): "cancelled" | "completed" | "failed" | null { + const terminal = this.#runEventTerminal; + return terminal?.runId === runId ? terminal.status : null; } async commandUpdate( @@ -213,7 +268,7 @@ export class DriverInstanceSocket { : { providerReceiptJson: input.providerReceiptJson }), result: input.result, }, - this.#rpcOptions(signal), + this.#effectSettlementRpcOptions(signal), ); } @@ -278,6 +333,35 @@ export class DriverInstanceSocket { events: DriverEventInput[]; signal?: AbortSignal; }): Promise { + const prepared = this.#prepareEventPush(input); + const task = this.#enqueueDelivery(() => this.#deliverEventPush(prepared), input.signal); + const selection = prepared.selection; + + if (selection !== null) { + selection.task = task; + } + + try { + const result = await task; + if ( + selection !== null && + result.accepted.length === prepared.events.length && + this.#runEventTerminal === selection + ) { + selection.delivered = true; + } + return result; + } finally { + if (selection?.task === task) { + delete selection.task; + } + } + } + + #prepareEventPush(input: { + events: DriverEventInput[]; + signal?: AbortSignal; + }): PreparedEventPush { input.signal?.throwIfAborted(); const maxBatchSize = this.#eventBatchMaxSize; @@ -285,28 +369,125 @@ export class DriverInstanceSocket { throw new Error("Driver hello must complete before events are pushed."); } - const events = input.events.flatMap((event) => - toDriverEventEnvelopes(this.payload, event, this.#activeRunId), + const activeRunId = this.#activeRunId; + const events = structuredClone(input.events).flatMap((event) => + toDriverEventEnvelopes(this.payload, event, activeRunId), ); - const accepted: DriverEventBatchOutput["accepted"][number][] = []; const delivery = events[0]?.event.delivery; + let terminal: { + event: Pick; + runId: RunId; + status: RunEventTerminalStatus; + } | null = null; + let terminalIndex = -1; + let hasRunScopedEvent = false; if (events.some((envelope) => envelope.event.delivery !== delivery)) { throw new Error("Driver event batches cannot mix lossless and best-effort delivery."); } - const rpcOptions = this.#rpcOptions(input.signal); + + for (const [index, { event }] of events.entries()) { + if (event.runId !== undefined) { + if (event.runId !== activeRunId) { + throw new Error("Driver event must target the active run."); + } + hasRunScopedEvent = true; + } + + const status = + event.kind === "run.cancelled" + ? "cancelled" + : event.kind === "run.completed" + ? "completed" + : event.kind === "run.failed" + ? "failed" + : null; + + if (status === null) { + continue; + } + if (event.runId === undefined) { + throw new Error("Driver run terminal must target a run."); + } + if (terminal !== null) { + throw new Error("Driver event batch cannot contain multiple run terminals."); + } + + terminal = { + event: { kind: event.kind, payload: structuredClone(event.payload) }, + runId: event.runId, + status, + }; + terminalIndex = index; + } + + if (terminalIndex >= 0 && terminalIndex !== events.length - 1) { + throw new Error("Driver run terminal must be the final event in its batch."); + } + if (terminal !== null && delivery === "best_effort") { + throw new Error("Driver run terminal must use lossless delivery."); + } + + let selection: RunEventTerminalSelection | null = null; + if (hasRunScopedEvent && this.#runEventTerminal !== null) { + const selected = this.#runEventTerminal; + const retryingSelectedEvent = + events.length === 1 && + terminal !== null && + selected !== null && + !selected.delivered && + selected.task === undefined && + selected.runId === terminal.runId && + selected.status === terminal.status && + isDeepStrictEqual(selected.event, terminal.event); + + if (!retryingSelectedEvent) { + throw new Error("Driver event cannot target a terminated run."); + } + selection = selected; + } else if (terminal !== null) { + selection = { + delivered: false, + event: terminal.event, + runId: terminal.runId, + status: terminal.status, + }; + this.#runEventTerminal = selection; + } + + return { + client: this.#requireClient(), + delivery, + events, + generation: this.#connectionGeneration, + hasRunScopedEvent, + maxBatchSize, + runGeneration: this.#runGeneration, + selection, + signal: input.signal, + }; + } + + async #deliverEventPush(prepared: PreparedEventPush): Promise { + const { client, delivery, events, generation, maxBatchSize, signal } = prepared; + const accepted: DriverEventBatchOutput["accepted"][number][] = []; + this.#assertConnection(client, generation, "event delivery"); + this.#assertRunGeneration(prepared); + const rpcOptions = this.#rpcOptions(signal); for (let index = 0; index < events.length; index += maxBatchSize) { let remaining = events.slice(index, index + maxBatchSize); while (remaining.length > 0) { - const result = await this.#requireClient().driver.pushEvents( + const result = await client.driver.pushEvents( { driverInstanceId: this.payload.driverInstanceId, events: remaining, }, rpcOptions, ); + this.#assertConnection(client, generation, "event delivery"); + this.#assertRunGeneration(prepared); assertDriverEventReceiptPrefix( remaining.map((envelope) => envelope.event), @@ -322,19 +503,6 @@ export class DriverInstanceSocket { } accepted.push(...result.accepted); - for (const { event } of remaining.slice(0, result.accepted.length)) { - const status = - event.kind === "run.cancelled" - ? "cancelled" - : event.kind === "run.completed" - ? "completed" - : event.kind === "run.failed" - ? "failed" - : null; - if (status !== null && event.runId !== undefined) { - this.#runEventTerminal = { runId: event.runId, status }; - } - } if (delivery === "best_effort" && result.accepted.length < remaining.length) { return { accepted }; @@ -405,7 +573,7 @@ export class DriverInstanceSocket { commandId: input.commandId, driverInstanceId: this.payload.driverInstanceId, }, - this.#rpcOptions(signal), + this.#effectSettlementRpcOptions(signal), ); } @@ -414,27 +582,29 @@ export class DriverInstanceSocket { error?: DriverFailureInput["error"], signal?: AbortSignal, ): Promise { + if (status === "failed" && error === undefined) { + return Promise.reject(new Error("Failed run terminal requires an error.")); + } + let terminal = this.#runTerminal; if (terminal === null) { - if (status === "failed" && error === undefined) { - return Promise.reject(new Error("Failed run terminal requires an error.")); - } - terminal = status === "completed" - ? { delivered: false, status } - : { delivered: false, error: structuredClone(error!), status }; + ? { delivered: false, runGeneration: this.#runGeneration, status } + : { + delivered: false, + error: structuredClone(error!), + runGeneration: this.#runGeneration, + status, + }; this.#runTerminal = terminal; } if (terminal.status !== status) { return Promise.resolve(); } - if ( - terminal.status === "failed" && - (error === undefined || !isDeepStrictEqual(terminal.error, error)) - ) { + if (terminal.status === "failed" && !isDeepStrictEqual(terminal.error, error)) { return Promise.reject(new Error("Failed run terminal was retried with a different error.")); } if (terminal.delivered) { @@ -446,11 +616,13 @@ export class DriverInstanceSocket { const client = this.#client; const generation = this.#connectionGeneration; - const options = this.#rpcOptions(signal); - const task = Promise.resolve().then(async () => { + const task = this.#enqueueDelivery(async () => { if (client === null) { throw new Error("Driver instance socket is not connected."); } + this.#assertConnection(client, generation, "run terminal delivery"); + this.#assertRunTerminal(terminal); + const options = this.#rpcOptions(signal); if (terminal.status === "completed") { await client.driver.completeRun( @@ -467,20 +639,19 @@ export class DriverInstanceSocket { ); } - if (generation !== this.#connectionGeneration || client !== this.#client) { - throw new Error("Driver socket connection changed during run terminal delivery."); - } - }); + this.#assertConnection(client, generation, "run terminal delivery"); + this.#assertRunTerminal(terminal); + }, signal); terminal.task = task; void task.then( () => { - if (terminal.task === task) { + if (this.#runTerminal === terminal && terminal.task === task) { terminal.delivered = true; delete terminal.task; } }, () => { - if (terminal.task === task) { + if (this.#runTerminal === terminal && terminal.task === task) { delete terminal.task; } }, @@ -488,6 +659,52 @@ export class DriverInstanceSocket { return task; } + #assertConnection(client: DriverRuntimeClient, generation: number, operation: string): void { + if (generation !== this.#connectionGeneration || client !== this.#client) { + throw new Error(`Driver socket connection changed during ${operation}.`); + } + } + + #assertRunGeneration(prepared: PreparedEventPush): void { + if (prepared.hasRunScopedEvent && prepared.runGeneration !== this.#runGeneration) { + throw new Error("Driver active run changed during event delivery."); + } + } + + #assertRunTerminal(terminal: RunTerminalDelivery): void { + if (terminal.runGeneration !== this.#runGeneration || this.#runTerminal !== terminal) { + throw new Error("Driver active run changed during run terminal delivery."); + } + } + + #enqueueDelivery(operation: () => Promise, signal?: AbortSignal): Promise { + const predecessor = this.#deliveryTail; + const slot = Promise.withResolvers(); + this.#deliveryTail = predecessor.then( + () => slot.promise, + () => slot.promise, + ); + + return (async () => { + let acquired = false; + + try { + await raceWithAbort(predecessor, signal); + acquired = true; + return await operation(); + } finally { + if (acquired) { + slot.resolve(); + } else { + void predecessor.then( + () => slot.resolve(), + () => slot.resolve(), + ); + } + } + })(); + } + #requireClient(): DriverRuntimeClient { if (!this.#client) { throw new Error("Driver instance socket is not connected."); @@ -508,4 +725,10 @@ export class DriverInstanceSocket { ]), }; } + + #effectSettlementRpcOptions(signal: AbortSignal): DriverRpcOptions { + return { + signal: AbortSignal.any([signal, AbortSignal.timeout(DRIVER_RPC_TIMEOUT_MS)]), + }; + } } diff --git a/src/models/index.ts b/src/models/index.ts deleted file mode 100644 index 4179bc3..0000000 --- a/src/models/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const OPENAI_DEFAULT_MODEL_ID = "gpt-5.6-luna"; diff --git a/src/observability/driver-debug-paths.ts b/src/observability/driver-debug-paths.ts index fda7b20..9953469 100644 --- a/src/observability/driver-debug-paths.ts +++ b/src/observability/driver-debug-paths.ts @@ -101,7 +101,6 @@ export function summarizeDriverBootPayload(payload: DriverBootPayload): Record { const record = readRecord(value, "execution.environment.variables"); - const variables: Record = {}; - - for (const [key, entry] of Object.entries(record)) { - if (typeof entry !== "string") { - throw new TypeError(`execution.environment.variables.${key} must be a string.`); + const entries = Object.entries(record); + + for (const [key, entry] of entries) { + if ( + key.length === 0 || + key.includes("=") || + key.includes("\0") || + typeof entry !== "string" || + entry.includes("\0") + ) { + throw new TypeError( + `execution.environment.variables.${key} must be a valid environment entry.`, + ); } - - variables[key] = entry; } - return variables; + return Object.fromEntries(entries) as Record; } function readAbsolutePathArray(record: Record, field: string): string[] { const label = "execution.environment.paths"; return readStringArray(record, field, label).map((path, index) => { - if (!path.startsWith("/") || path.includes("\0")) { + if (!isAbsolute(path) || path.includes("\0") || path.includes(delimiter)) { throw new TypeError( - `${label}.${field}[${index}] must be an absolute path without null bytes.`, + `${label}.${field}[${index}] must be an absolute path without null bytes or path delimiters.`, ); } @@ -534,18 +542,25 @@ export function parseDriverBootPayload(value: unknown): DriverBootPayload { throw new TypeError("Driver boot payload.controlUrl must be an absolute URL."); } + const execution = readExecution(record["execution"]); + const sandboxId = parseId(record["sandboxId"], "Driver sandbox ID") as SandboxId; + + if (sandboxId !== execution.session.context.sandboxId) { + throw new TypeError("Driver boot payload sandbox IDs must match."); + } + return { bootToken: readNonEmptyString(record, "bootToken", "Driver boot payload"), controlUrl, driverControlPort, driverGeneration, driverInstanceId: parseId(record["driverInstanceId"], "Driver instance ID") as DriverInstanceId, - execution: readExecution(record["execution"]), + execution, heartbeatIntervalMs, protocolVersion, runtime, runtimeTransport, - sandboxId: parseId(record["sandboxId"], "Driver sandbox ID") as SandboxId, + sandboxId, traceparent: readNonEmptyString(record, "traceparent", "Driver boot payload"), }; } diff --git a/src/protocol/events/runtime-event-validation.ts b/src/protocol/events/runtime-event-validation.ts index e35b002..89d62b0 100644 --- a/src/protocol/events/runtime-event-validation.ts +++ b/src/protocol/events/runtime-event-validation.ts @@ -1,4 +1,4 @@ -import { jsonValueSchema, timestampSchema } from "../../contract/common"; +import { timestampSchema } from "../../contract/common"; import type { DriverId } from "../id"; import { parseDriverId } from "../id"; import type { @@ -90,15 +90,9 @@ export function hasRunStartedAt(record: RuntimeEventRecord): boolean { } export function omitPayloadIdentity(payload: RuntimeEventRecord): RuntimeEventRecord { - const result: RuntimeEventRecord = {}; - - for (const [key, value] of Object.entries(payload)) { - if (!payloadIdentityFields.has(key)) { - result[key] = value; - } - } - - return result; + return Object.fromEntries( + Object.entries(payload).filter(([key]) => !payloadIdentityFields.has(key)), + ); } export function requirePayloadRecord( @@ -186,20 +180,6 @@ export function requireOptionalBoolean( } } -export function requireOptionalJsonValue( - value: RuntimeEventRecord, - field: string, - label: RuntimeEventKind | string, -): void { - if (!(field in value) || value[field] === undefined) { - return; - } - - if (!jsonValueSchema.safeParse(value[field]).success) { - throw new Error(`${label} ${field} must be JSON-serializable.`); - } -} - export function requireOptionalContentString( value: RuntimeEventRecord, field: string, @@ -345,18 +325,13 @@ export function readPrimitiveRecord( return {}; } - const result: Record = {}; - - for (const [key, entry] of Object.entries(value)) { - if ( - entry === null || - typeof entry === "string" || - typeof entry === "number" || - typeof entry === "boolean" - ) { - result[key] = entry; - } - } - - return result; + return Object.fromEntries( + Object.entries(value).filter( + ([, entry]) => + entry === null || + typeof entry === "string" || + typeof entry === "number" || + typeof entry === "boolean", + ), + ) as Record; } diff --git a/src/protocol/events/runtime-events.ts b/src/protocol/events/runtime-events.ts index 204b0af..e6f980c 100644 --- a/src/protocol/events/runtime-events.ts +++ b/src/protocol/events/runtime-events.ts @@ -1,3 +1,4 @@ +import { jsonValueSchema } from "../../contract/common"; import type { DriverInstanceId, EventId, SessionId, RunId } from "../id"; import { RUNTIME_EVENT_KINDS, @@ -37,7 +38,6 @@ import { requireOptionalBoolean, requireOptionalContentString, requireOptionalEnumValue, - requireOptionalJsonValue, requireOptionalNullableString, requireOptionalString, requireOptionalStringArray, @@ -106,9 +106,7 @@ const runStatuses = new Set([ ]); const toolStatuses = new Set(["cancelled", "completed", "failed", "running"]); const fileChangeKinds = new Set(["delete", "upsert"]); -export { isRuntimeEventRecord } from "./runtime-event-validation"; - -export function createRuntimeEvent( +function createRuntimeEvent( draft: RuntimeEventDraft, ): RuntimeEventEnvelope { return { @@ -337,9 +335,21 @@ function admitRuntimeEventPayload( }, payload: unknown, ): unknown { + let canonicalPayload: unknown; + + try { + canonicalPayload = structuredClone(payload); + } catch { + throw new Error(`Runtime event ${context.kind} payload must be JSON-serializable.`); + } + + if (!jsonValueSchema.safeParse(canonicalPayload).success) { + throw new Error(`Runtime event ${context.kind} payload must be JSON-serializable.`); + } + switch (context.kind) { case "agent.task.updated": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); requireString(record, "taskId", context.kind); requireOptionalBoolean(record, "active", context.kind); requireOptionalEnumValue(record, "status", toolStatuses, context.kind); @@ -349,7 +359,7 @@ function admitRuntimeEventPayload( return omitPayloadIdentity(record); } case "diagnostic.reported": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); requireOptionalString(record, "code", context.kind); requireOptionalString(record, "message", context.kind); requireOptionalString(record, "severity", context.kind); @@ -357,7 +367,7 @@ function admitRuntimeEventPayload( } case "file.change.updated": case "file.changed": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); const changes = Array.isArray(record["changes"]) ? record["changes"] : [record]; if (changes.length === 0) { @@ -374,7 +384,7 @@ function admitRuntimeEventPayload( } case "message.added": case "message.delta": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); if (!hasTextContent(record)) { throw new Error(`Runtime event ${context.kind} payload must include text content.`); @@ -393,8 +403,6 @@ function admitRuntimeEventPayload( requireOptionalString(record, "toolCallId", context.kind); requireOptionalEnumValue(record, "phase", new Set(["commentary", "final"]), context.kind); - requireOptionalJsonValue(record, "memoryCitation", context.kind); - return omitPayloadIdentity(record); } case "message.cancelled": @@ -403,14 +411,14 @@ function admitRuntimeEventPayload( case "thought.cancelled": case "thought.completed": case "thought.started": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); requireOptionalString(record, "messageId", context.kind); requireOptionalString(record, "thoughtId", context.kind); requireOptionalEnumValue(record, "role", new Set(["agent", "user"]), context.kind); return omitPayloadIdentity(record); } case "message.failed": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); requireString(record, "messageId", context.kind); requireOptionalEnumValue(record, "role", new Set(["agent", "user"]), context.kind); const admitted = omitPayloadIdentity(record); @@ -418,7 +426,7 @@ function admitRuntimeEventPayload( return admitted; } case "thought.delta": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); if (!hasTextContent(record)) { throw new Error("Runtime event thought.delta payload must include text content."); @@ -436,7 +444,7 @@ function admitRuntimeEventPayload( throw new Error("Runtime event permission.requested requires a run ID."); } - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); requireString(record, "requestId", context.kind); requireString(record, "title", context.kind); requireOptionalString(record, "agentId", context.kind); @@ -474,7 +482,7 @@ function admitRuntimeEventPayload( return omitPayloadIdentity(record); } case "permission.resolved": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); requireString(record, "requestId", context.kind); requireString(record, "outcome", context.kind); requireOptionalString(record, "optionId", context.kind); @@ -482,7 +490,7 @@ function admitRuntimeEventPayload( return omitPayloadIdentity(record); } case "session.info.updated": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); requireOptionalNullableString(record, "title", context.kind); if (record["updatedAt"] !== undefined) { requireNullableTimestamp(record, "updatedAt", context.kind, "updatedAt"); @@ -498,14 +506,14 @@ function admitRuntimeEventPayload( case "run.started": case "run.steered": case "run.waiting": { - return readRunPayload(context, payload); + return readRunPayload(context, canonicalPayload); } case "runtime.config.updated": case "runtime.driver.updated": case "runtime.provisioning.updated": case "runtime.sandbox.updated": case "runtime.transport.updated": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); requireString(record, "status", context.kind); if (context.kind === "runtime.transport.updated") { @@ -517,10 +525,10 @@ function admitRuntimeEventPayload( return omitPayloadIdentity(record); } case "runtime.timing.recorded": { - return readTimingPayload(context, payload); + return readTimingPayload(context, canonicalPayload); } case "tool.call.updated": { - const record = requirePayloadRecord(context.kind, payload); + const record = requirePayloadRecord(context.kind, canonicalPayload); requireEnumValue(record, "status", toolStatuses, context.kind); requireString(record, "toolCallId", context.kind); requireOptionalContentString(record, "content", context.kind); @@ -534,13 +542,14 @@ function admitRuntimeEventPayload( requireOptionalString(record, "parentMessageId", context.kind); requireOptionalString(record, "rawInput", context.kind); requireOptionalString(record, "rawOutput", context.kind); - requireOptionalJsonValue(record, "structuredOutput", context.kind); requireOptionalNullableString(record, "title", context.kind); requireOptionalString(record, "userFeedback", context.kind); return omitPayloadIdentity(record); } default: { - return isRuntimeEventRecord(payload) ? omitPayloadIdentity(payload) : payload; + return isRuntimeEventRecord(canonicalPayload) + ? omitPayloadIdentity(canonicalPayload) + : canonicalPayload; } } } @@ -568,7 +577,6 @@ function readRunPayload( requireOptionalString(record, "targetRunId", context.kind); requireOptionalString(record, "userMessageId", context.kind); requireOptionalStringArray(record, "inputItemIds", context.kind); - requireOptionalJsonValue(record, "structuredOutput", context.kind); requireOptionalTimestamp(record, "completedAt", context.kind); requireOptionalTimestamp(record, "startedAt", context.kind); diff --git a/src/protocol/paths/index.ts b/src/protocol/paths/index.ts index c9af444..8efa807 100644 --- a/src/protocol/paths/index.ts +++ b/src/protocol/paths/index.ts @@ -7,8 +7,6 @@ export const SANDBOX_SESSION_ROOT = `${SANDBOX_WORKSPACE_ROOT}/se`; const SESSION_RESOURCE_MOUNT_DIR = "session-files"; -export type SandboxFileBrowserPathPurpose = "content" | "tree"; - export function getSessionWorkspacePath(sessionId: string): string { return `${SANDBOX_SESSION_ROOT}/${sessionId}`; } @@ -135,10 +133,7 @@ function isAllowedSandboxFileBrowserPath(path: string): boolean { ); } -export function normalizeSandboxFileBrowserPath( - rawPath: string, - _purpose: SandboxFileBrowserPathPurpose, -): string { +export function normalizeSandboxFileBrowserPath(rawPath: string): string { const path = readSandboxFileBrowserPathOriginal(rawPath); if (isSandboxCachePath(path)) { diff --git a/src/protocol/start.ts b/src/protocol/start.ts index db5f6db..7016b37 100644 --- a/src/protocol/start.ts +++ b/src/protocol/start.ts @@ -21,6 +21,6 @@ export function createDriverStartInputFromBootPayload( execution: createDriverExecutionInputFromBootExecution(payload.execution), runtime: payload.runtime, runtimeTransport: payload.runtimeTransport, - sandboxId: payload.sandboxId, + sandboxId: payload.execution.session.context.sandboxId, }; } diff --git a/src/runtimes/acp/acp-agent-process.ts b/src/runtimes/acp/acp-agent-process.ts index 75b2db0..b77de8b 100644 --- a/src/runtimes/acp/acp-agent-process.ts +++ b/src/runtimes/acp/acp-agent-process.ts @@ -48,7 +48,10 @@ export async function startAcpAgentProcess( env: Record, signal: AbortSignal, supervisionOptions: AcpAgentProcessSupervisionOptions = {}, -): Promise { +): Promise<{ + readonly process: AcpAgentProcess; + readonly ready: Promise; +}> { const command = supervisionOptions.command ?? readFallbackCommand(); const args = supervisionOptions.args ?? readFallbackArgs(); @@ -123,14 +126,28 @@ export async function startAcpAgentProcess( }); }); + const ready = waitForAcpAgentProcessReady( + context, + agentProcess, + supervision, + signal, + supervisionOptions.spawnWatchdog, + ); + void ready.catch(() => {}); + + return { process: agentProcess, ready }; +} + +async function waitForAcpAgentProcessReady( + context: AgentDriverContext, + agentProcess: AcpAgentProcess, + supervision: AcpAgentProcessSupervision, + signal: AbortSignal, + spawnWatchdog: typeof spawnLinuxProcessTreeWatchdog = spawnLinuxProcessTreeWatchdog, +): Promise { try { supervision.watchdog = - agentProcess.pid === undefined - ? null - : (supervisionOptions.spawnWatchdog ?? spawnLinuxProcessTreeWatchdog)( - agentProcess.pid, - processTree.marker, - ); + agentProcess.pid === undefined ? null : spawnWatchdog(agentProcess.pid, supervision.marker); if (supervision.watchdog !== null) { void supervision.watchdog.cleanup.then( () => { @@ -165,21 +182,8 @@ export async function startAcpAgentProcess( signal.throwIfAborted(); } catch (error) { signalAcpAgentProcess(agentProcess, "SIGKILL"); - try { - await stopAcpAgentProcess( - context, - agentProcess, - "startup.failed", - Date.now() + ACP_AGENT_EXIT_TIMEOUT_MS + ACP_AGENT_FORCE_KILL_TIMEOUT_MS, - signal, - ); - } catch (cleanupError) { - throw new AggregateError([error, cleanupError], "ACP agent process startup cleanup failed."); - } throw error; } - - return agentProcess; } export async function stopAcpAgentProcess( diff --git a/src/runtimes/acp/acp-assistant-transcript-state.ts b/src/runtimes/acp/acp-assistant-transcript-state.ts index a4f9ddd..ca3776b 100644 --- a/src/runtimes/acp/acp-assistant-transcript-state.ts +++ b/src/runtimes/acp/acp-assistant-transcript-state.ts @@ -1,9 +1,13 @@ import type { StopReason } from "@agentclientprotocol/sdk"; import type { DriverEventInput } from "../../protocol/events"; +import { createDriverId } from "../../protocol/id"; import type { RunId } from "../../protocol/id"; +import { + MAX_RUN_TERMINAL_BATCH_BYTES, + MAX_RUN_TERMINAL_BATCH_EVENTS, +} from "../driver-event-admission"; import { chunkJsonText } from "../provider-json"; -import { RuntimeAssistantMessageIdIndex } from "../runtime-turn-transcript"; import { toPermissionRequest } from "./acp-permission-events"; import type { AcpPermissionTranslation } from "./acp-permission-events"; import { @@ -28,12 +32,28 @@ import { import type { JsonObject } from "./acp-types"; const MAX_ACP_MESSAGE_EVENT_TEXT_BYTES = MAX_ACP_LOSSLESS_EVENT_BYTES - 1_024; +// An open item can require two terminal closures. Reserve four shared-batch +// slots for message/thought/final envelopes. A retained byte (notably a tool +// ID) can occur in both closures, so reserve one quarter of the byte budget for +// envelopes and admit half of the remaining three quarters. Measuring current +// retained state makes replay/no-op traffic free while keeping every possible +// ACP settlement below the shared 1024-event / 1 MiB hard limit. +const MAX_ACP_RETAINED_TURN_BYTES = (MAX_RUN_TERMINAL_BATCH_BYTES * 3) / 8; +const MAX_ACP_RETAINED_TURN_ITEMS = Math.floor((MAX_RUN_TERMINAL_BATCH_EVENTS - 4) / 2); +// Settled native IDs are only a replay-suppression cache. Bound and evict them +// independently; they cannot produce terminal closures. +const MAX_ACP_SETTLED_MESSAGE_HISTORY_BYTES = MAX_ACP_LOSSLESS_EVENT_BYTES; +const MAX_ACP_SETTLED_MESSAGE_HISTORY_ITEMS = 1_024; export interface AcpAssistantTranscriptStateInput { readonly messageId: string; readonly runId: RunId; } +export class AcpTurnStateLimitError extends RangeError { + override readonly name = "AcpTurnStateLimitError"; +} + interface AcpAssistantMessageState { readonly id: string; readonly nativeMessageId: string | null; @@ -47,13 +67,13 @@ interface AcpAssistantMessageStart { export class AcpAssistantTranscriptState { #activeAssistantMessage: AcpAssistantMessageState | null = null; - readonly #assistantMessageIds = new RuntimeAssistantMessageIdIndex(); - #lastCompletedAssistantMessage: Pick | null = null; + #lastCompletedAssistantMessage: { readonly hasVisibleText: boolean; readonly id: string } | null = + null; #promptMessageId: string | null = null; #runId: RunId | null = null; #sequence = 0; readonly #settledAssistantNativeMessageIds = new Set(); - #unidentifiedAssistantMessageSequence = 0; + #settledAssistantNativeMessageIdBytes = 0; #thoughtCompleted = false; #thoughtFallbackText = ""; #thoughtFallbackNativeMessageId: string | null = null; @@ -67,13 +87,12 @@ export class AcpAssistantTranscriptState { begin(input: AcpAssistantTranscriptStateInput): void { this.#activeAssistantMessage = null; - this.#assistantMessageIds.reset(); this.#lastCompletedAssistantMessage = null; this.#promptMessageId = input.messageId; this.#runId = input.runId; this.#sequence = 0; this.#settledAssistantNativeMessageIds.clear(); - this.#unidentifiedAssistantMessageSequence = 0; + this.#settledAssistantNativeMessageIdBytes = 0; this.#thoughtCompleted = false; this.#thoughtFallbackText = ""; this.#thoughtFallbackNativeMessageId = null; @@ -84,13 +103,12 @@ export class AcpAssistantTranscriptState { clear(): void { this.#activeAssistantMessage = null; - this.#assistantMessageIds.reset(); this.#lastCompletedAssistantMessage = null; this.#promptMessageId = null; this.#runId = null; this.#sequence = 0; this.#settledAssistantNativeMessageIds.clear(); - this.#unidentifiedAssistantMessageSequence = 0; + this.#settledAssistantNativeMessageIdBytes = 0; this.#thoughtCompleted = false; this.#thoughtFallbackText = ""; this.#thoughtFallbackNativeMessageId = null; @@ -108,11 +126,11 @@ export class AcpAssistantTranscriptState { : { ...this.#lastCompletedAssistantMessage }; const sequence = this.#sequence; const settledAssistantNativeMessageIds = new Set(this.#settledAssistantNativeMessageIds); + const settledAssistantNativeMessageIdBytes = this.#settledAssistantNativeMessageIdBytes; const thoughtCompleted = this.#thoughtCompleted; const thoughtFallbackNativeMessageId = this.#thoughtFallbackNativeMessageId; const thoughtFallbackText = this.#thoughtFallbackText; const thoughtStarted = this.#thoughtStarted; - const unidentifiedAssistantMessageSequence = this.#unidentifiedAssistantMessageSequence; const restoreTools = this.#tools.checkpoint(); return () => { @@ -123,11 +141,11 @@ export class AcpAssistantTranscriptState { settledAssistantNativeMessageIds.forEach((id) => this.#settledAssistantNativeMessageIds.add(id), ); + this.#settledAssistantNativeMessageIdBytes = settledAssistantNativeMessageIdBytes; this.#thoughtCompleted = thoughtCompleted; this.#thoughtFallbackNativeMessageId = thoughtFallbackNativeMessageId; this.#thoughtFallbackText = thoughtFallbackText; this.#thoughtStarted = thoughtStarted; - this.#unidentifiedAssistantMessageSequence = unidentifiedAssistantMessageSequence; restoreTools(); }; } @@ -188,7 +206,7 @@ export class AcpAssistantTranscriptState { const finalMessage = this.#lastCompletedAssistantMessage; const emptyTurn = stopReason === "end_turn" && - (finalMessage === null || finalMessage.text.trim().length === 0) && + (finalMessage === null || !finalMessage.hasVisibleText) && !this.#tools.hasActivity(); if (stopReason === "cancelled") { @@ -277,7 +295,6 @@ export class AcpAssistantTranscriptState { events.push( ...this.#tools.completeOpen({ - error: message, runId, status: "failed", }), @@ -303,98 +320,114 @@ export class AcpAssistantTranscriptState { const record = isRecord(params) ? params : {}; const update = readRecord(record, "update"); const sessionUpdate = readString(update, "sessionUpdate"); - - let events: DriverEventInput[]; - - switch (sessionUpdate) { - case "agent_message_chunk": { - events = this.#messageChunk(update); - break; - } - case "agent_thought_chunk": { - events = this.#thoughtChunk(update); - break; - } - case "available_commands_update": { - events = toCommandEvents(update); - break; - } - case "config_option_update": { - events = toConfigEvents(update); - break; - } - case "current_mode_update": { - events = toModeEvents(update); - break; - } - case "plan": { - events = toPlanEvents(update); - break; - } - case "session_info_update": { - events = toInfoEvents(update); - break; - } - case "tool_call": - case "tool_call_update": { - events = this.#tool(update, sessionUpdate); - break; - } - case "usage_update": { - events = toUsageEvents(update); - break; - } - case "user_message_chunk": - case undefined: - case null: { - events = []; - break; - } - default: { - events = [ - { - delivery: "best_effort", - kind: "diagnostic.reported", - payload: { - message: `Unsupported ACP session update: ${sessionUpdate}.`, - raw: update, - severity: "info", + const retainsTurnState = + this.#runId !== null && + (sessionUpdate === "agent_message_chunk" || + sessionUpdate === "agent_thought_chunk" || + sessionUpdate === "tool_call" || + sessionUpdate === "tool_call_update"); + const restore = retainsTurnState ? this.checkpoint() : null; + + try { + let events: DriverEventInput[]; + + switch (sessionUpdate) { + case "agent_message_chunk": { + events = this.#messageChunk(update); + break; + } + case "agent_thought_chunk": { + events = this.#thoughtChunk(update); + break; + } + case "available_commands_update": { + events = toCommandEvents(update); + break; + } + case "config_option_update": { + events = toConfigEvents(update); + break; + } + case "current_mode_update": { + events = toModeEvents(update); + break; + } + case "plan": { + events = toPlanEvents(update); + break; + } + case "session_info_update": { + events = toInfoEvents(update); + break; + } + case "tool_call": + case "tool_call_update": { + events = this.#tool(update, sessionUpdate); + break; + } + case "usage_update": { + events = toUsageEvents(update); + break; + } + case "user_message_chunk": + case undefined: + case null: { + events = []; + break; + } + default: { + events = [ + { + delivery: "best_effort", + kind: "diagnostic.reported", + payload: { + message: `Unsupported ACP session update: ${sessionUpdate}.`, + raw: update, + severity: "info", + }, + visibility: "owner_debug", }, - visibility: "owner_debug", - }, - ]; - break; + ]; + break; + } } - } - return assertBoundedLosslessEvents(events); + if (retainsTurnState) { + this.#assertRetainedTurnState(); + } + return assertBoundedLosslessEvents(events); + } catch (error) { + restore?.(); + throw error; + } } translatePermission(input: { params: unknown; requestId: string }): AcpPermissionTranslation { const runId = this.activeRunId(); - const translation = toPermissionRequest({ - params: input.params, - requestId: input.requestId, - runId, - }); + const restore = runId === null ? null : this.checkpoint(); - if (runId === null || translation.toolCall === null) { - return { - ...translation, - events: assertBoundedLosslessEvents(translation.events), - }; - } + try { + const translation = toPermissionRequest({ + params: input.params, + requestId: input.requestId, + runId, + }); - this.#tools.patch({ - status: toRuntimeToolStatus(readString(translation.toolCall, "status")), - toolCallId: translation.targetItemId, - update: translation.toolCall, - }); + if (runId === null || translation.toolCall === null) { + return { + ...translation, + events: assertBoundedLosslessEvents(translation.events), + }; + } - return { - ...translation, - events: assertBoundedLosslessEvents([ - ...this.#ensureToolParentMessage(translation.targetItemId), + const parentStart = this.#ensureToolParentMessage(translation.targetItemId); + this.#tools.patch({ + status: toRuntimeToolStatus(readString(translation.toolCall, "status")), + toolCallId: translation.targetItemId, + update: translation.toolCall, + }); + const events = [ + ...parentStart, ...this.#tools.ensureStarted({ parentMessageId: this.#toolParentMessageId(), runId, @@ -402,8 +435,17 @@ export class AcpAssistantTranscriptState { toolCallId: translation.targetItemId, }), ...translation.events, - ]), - }; + ]; + this.#assertRetainedTurnState(); + + return { + ...translation, + events: assertBoundedLosslessEvents(events), + }; + } catch (error) { + restore?.(); + throw error; + } } #nextEventId(kind: string): string { @@ -601,10 +643,10 @@ export class AcpAssistantTranscriptState { this.#activeAssistantMessage = null; if (message.nativeMessageId !== null) { - this.#settledAssistantNativeMessageIds.add(message.nativeMessageId); + this.#rememberSettledAssistantMessage(message.nativeMessageId); this.#lastCompletedAssistantMessage = { + hasVisibleText: message.text.trim().length > 0, id: message.id, - text: message.text, }; } else { // ACP v1 does not give anonymous chunks a stable message boundary. If @@ -665,7 +707,7 @@ export class AcpAssistantTranscriptState { this.#lastCompletedAssistantMessage = null; if (message.nativeMessageId !== null) { - this.#settledAssistantNativeMessageIds.add(message.nativeMessageId); + this.#rememberSettledAssistantMessage(message.nativeMessageId); } if (stopReason === "cancelled") { @@ -711,7 +753,7 @@ export class AcpAssistantTranscriptState { this.#lastCompletedAssistantMessage = null; if (message.nativeMessageId !== null) { - this.#settledAssistantNativeMessageIds.add(message.nativeMessageId); + this.#rememberSettledAssistantMessage(message.nativeMessageId); } return [ @@ -744,7 +786,7 @@ export class AcpAssistantTranscriptState { const events = this.#finishMessage(); const message: AcpAssistantMessageState = { - id: this.#assistantMessageIds.getOrCreate(`native:${nativeMessageId}`), + id: createDriverId(), nativeMessageId, text: "", }; @@ -770,11 +812,8 @@ export class AcpAssistantTranscriptState { } const events = this.#finishMessage(); - this.#unidentifiedAssistantMessageSequence += 1; const message: AcpAssistantMessageState = { - id: this.#assistantMessageIds.getOrCreate( - `fallback:unidentified:${this.#unidentifiedAssistantMessageSequence}`, - ), + id: createDriverId(), nativeMessageId: null, text: "", }; @@ -830,6 +869,66 @@ export class AcpAssistantTranscriptState { return this.#activeAssistantMessage?.id ?? this.#promptMessageId ?? undefined; } + #assertRetainedTurnState(): void { + this.#tools.compactHistory(); + const assistantItems = + (this.#activeAssistantMessage === null ? 0 : 1) + + (this.#thoughtFallbackNativeMessageId === null ? 0 : 1); + const itemCount = assistantItems + this.#tools.openItemCount(); + + if (itemCount > MAX_ACP_RETAINED_TURN_ITEMS) { + throw new AcpTurnStateLimitError( + `ACP turn state exceeds ${MAX_ACP_RETAINED_TURN_ITEMS} retained open items.`, + ); + } + + const retainedState = { + activeAssistantMessage: this.#activeAssistantMessage, + thoughtFallback: + this.#thoughtFallbackNativeMessageId === null + ? null + : { + nativeMessageId: this.#thoughtFallbackNativeMessageId, + text: this.#thoughtFallbackText, + }, + tools: this.#tools.retainedOpenState(), + }; + const bytes = Buffer.byteLength(JSON.stringify(retainedState), "utf8"); + + if (bytes > MAX_ACP_RETAINED_TURN_BYTES) { + throw new AcpTurnStateLimitError( + `ACP turn state exceeds ${MAX_ACP_RETAINED_TURN_BYTES} retained UTF-8 bytes.`, + ); + } + } + + #rememberSettledAssistantMessage(nativeMessageId: string): void { + if (this.#settledAssistantNativeMessageIds.has(nativeMessageId)) { + return; + } + + this.#settledAssistantNativeMessageIds.add(nativeMessageId); + this.#settledAssistantNativeMessageIdBytes += Buffer.byteLength( + JSON.stringify(nativeMessageId), + "utf8", + ); + + while ( + this.#settledAssistantNativeMessageIds.size > MAX_ACP_SETTLED_MESSAGE_HISTORY_ITEMS || + this.#settledAssistantNativeMessageIdBytes > MAX_ACP_SETTLED_MESSAGE_HISTORY_BYTES + ) { + const oldest = this.#settledAssistantNativeMessageIds.values().next().value; + if (oldest === undefined) { + break; + } + this.#settledAssistantNativeMessageIds.delete(oldest); + this.#settledAssistantNativeMessageIdBytes -= Buffer.byteLength( + JSON.stringify(oldest), + "utf8", + ); + } + } + // Tool calls must be parented to an assistant message: the session event // projection drops tool starts without a parent, and the prompt message id // would attach them to the user's bubble. ACP agents may open a tool call diff --git a/src/runtimes/acp/acp-client-request-handler.ts b/src/runtimes/acp/acp-client-request-handler.ts index 244b416..293eb47 100644 --- a/src/runtimes/acp/acp-client-request-handler.ts +++ b/src/runtimes/acp/acp-client-request-handler.ts @@ -21,11 +21,14 @@ import type { import type { DriverEventInput } from "../../protocol/events"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; -import type { AcpAssistantTranscriptState } from "./acp-assistant-transcript-state"; +import { + AcpTurnStateLimitError, + type AcpAssistantTranscriptState, +} from "./acp-assistant-transcript-state"; import { AcpFileSystem } from "./acp-file-system"; import { AcpPathScope } from "./acp-path-scope"; import type { AcpPermissionOption, AcpPermissionTranslation } from "./acp-permission-events"; -import { shouldIgnoreReplay } from "./acp-session-events"; +import { isTurnScopedSessionUpdate } from "./acp-session-events"; import { AcpSessionUpdateInbox, type AcpSessionUpdateScope } from "./acp-session-update-inbox"; import { AcpTerminalManager } from "./acp-terminal-manager"; import { isRecord, raceWithAbort, readNonEmptyString } from "./acp-types"; @@ -45,12 +48,14 @@ export class AcpClientRequestHandler { readonly #fileSystem: AcpFileSystem; readonly #isCancelling: () => boolean; readonly #nativeSessionId: () => string | null; + readonly #onUpdateFailure: (error: Error) => void; + readonly #pathScope: AcpPathScope; readonly #pendingPermissions = new Set>(); readonly #push: AcpClientRequestHandlerOptions["push"]; #permissionIngressClosed = false; #stopping = false; #transcriptTail: Promise = Promise.resolve(); - #turnUpdateIngressClosed = false; + #turnTranscriptIngressClosed = false; readonly #updateInbox: AcpSessionUpdateInbox; readonly #terminalManager: AcpTerminalManager; readonly #turnEvents: AcpAssistantTranscriptState; @@ -58,22 +63,23 @@ export class AcpClientRequestHandler { constructor(options: AcpClientRequestHandlerOptions) { this.#isCancelling = options.isCancelling; this.#nativeSessionId = options.nativeSessionId; + this.#onUpdateFailure = options.onUpdateFailure; this.#push = options.push; this.#turnEvents = options.turnEvents; - const pathScope = new AcpPathScope({ + this.#pathScope = new AcpPathScope({ allowedRoots: options.allowedRoots, cwd: options.cwd, }); this.#fileSystem = new AcpFileSystem({ allowedRoots: options.allowedRoots, cwd: options.cwd, - pathScope, + pathScope: this.#pathScope, }); this.#terminalManager = new AcpTerminalManager({ allowedRoots: options.allowedRoots, cwd: options.cwd, env: options.env, - pathScope, + pathScope: this.#pathScope, push: options.push, }); this.#updateInbox = new AcpSessionUpdateInbox({ @@ -82,8 +88,16 @@ export class AcpClientRequestHandler { }); } + initializePathScope(): Promise { + return this.#pathScope.initialize(); + } + + closePathScope(): Promise { + return this.#pathScope.close(); + } + enqueueUpdate(context: AgentDriverContext, notification: SessionNotification): Promise { - if (this.#turnUpdateIngressClosed) { + if (this.#turnTranscriptIngressClosed && isTurnScopedSessionUpdate(notification)) { return Promise.resolve(); } @@ -229,8 +243,8 @@ export class AcpClientRequestHandler { this.#permissionIngressClosed = true; } - closeTurnUpdateIngress(): void { - this.#turnUpdateIngressClosed = true; + closeTurnTranscriptIngress(): void { + this.#turnTranscriptIngressClosed = true; } async drainUpdates(): Promise { @@ -256,8 +270,8 @@ export class AcpClientRequestHandler { this.#permissionIngressClosed = false; } - openTurnUpdateIngress(): void { - this.#turnUpdateIngressClosed = false; + openTurnTranscriptIngress(): void { + this.#turnTranscriptIngressClosed = false; } async withSessionReplay(operation: () => Promise): Promise { @@ -299,7 +313,12 @@ export class AcpClientRequestHandler { return translated; }), signal, - ); + ).catch((error: unknown) => { + if (error instanceof AcpTurnStateLimitError) { + this.#onUpdateFailure(error); + } + throw error; + }); let chosen: AcpPermissionOption | null = null; @@ -328,13 +347,9 @@ export class AcpClientRequestHandler { ): Promise { this.#assertSession("session/update", params); - if (scope.suppressed && shouldIgnoreReplay(params)) { - return; - } - if ( - (scope.replaying || this.#turnEvents.activeRunId() === null) && - shouldIgnoreReplay(params) + (scope.suppressed || scope.replaying || this.#turnEvents.activeRunId() === null) && + isTurnScopedSessionUpdate(params) ) { return; } diff --git a/src/runtimes/acp/acp-driver-backend.ts b/src/runtimes/acp/acp-driver-backend.ts index 89da6fb..65c394b 100644 --- a/src/runtimes/acp/acp-driver-backend.ts +++ b/src/runtimes/acp/acp-driver-backend.ts @@ -26,10 +26,10 @@ import { DriverEventPublisher } from "../driver-event-publisher"; import { buildRuntimeBootstrapText, computeRuntimeBootstrapDigest, + exposeNativeSkillAliases, writeNativeRuntimeSystemPrompt, writeSkillBootstrapArtifacts, } from "../skill-bootstrap"; -import { exposeNativeSkillAliases } from "../skill-materialization"; import { startAcpAgentProcess, stopAcpAgentProcess } from "./acp-agent-process"; import type { AcpAgentProcess } from "./acp-agent-process"; import { AcpClientRequestHandler } from "./acp-client-request-handler"; @@ -70,6 +70,8 @@ export class AcpDriverBackend implements AgentDriverBackend { #agentCapabilities: AgentCapabilities | null = null; #agentLaunch: { readonly args: readonly string[]; readonly command: string } | null = null; #agentProcess: AcpAgentProcess | null = null; + #agentProcessStop: { readonly process: AcpAgentProcess; readonly task: Promise } | null = + null; readonly #childProcessEnv: Record; readonly #clientRequests: AcpClientRequestHandler; #connection: ClientConnection | null = null; @@ -81,7 +83,9 @@ export class AcpDriverBackend implements AgentDriverBackend { readonly #runtimeBootstrapDigest: string | null; readonly #runtimeBootstrapText: string; #started = false; + #stopFailure: { readonly error: unknown } | null = null; #stopRequested = false; + #stopSucceeded = false; #stopTask: Promise | null = null; readonly #turnController: AcpTurnController; @@ -93,7 +97,8 @@ export class AcpDriverBackend implements AgentDriverBackend { this.#runtimeBootstrapText = buildRuntimeBootstrapText(payload.execution); this.#turnController = new AcpTurnController( (context, reason, events) => this.#push(context, reason, events), - (context) => this.#recycleCancelledTurn(context), + (context, providerPromptAdmitted) => + this.#settleCancelledTurn(context, providerPromptAdmitted), (context, reason, closures, terminal) => this.#eventPublisher.pushTerminal(context, reason, closures, terminal), ); @@ -115,39 +120,43 @@ export class AcpDriverBackend implements AgentDriverBackend { throw new Error("ACP driver backend cannot restart after stopping."); } - const hostSnapshot = await raceWithAbort(context.ports.hostIntegration.snapshot(), signal); + try { + await raceWithAbort(this.#clientRequests.initializePathScope(), signal); + const hostSnapshot = await raceWithAbort(context.ports.hostIntegration.snapshot(), signal); - if (hostSnapshot === null) { - throw new Error("ACP fallback requires a host integration snapshot."); - } + if (hostSnapshot === null) { + throw new Error("ACP fallback requires a host integration snapshot."); + } - this.#hostSnapshot = hostSnapshot; - const materializedSkills = await raceWithAbort( - context.ports.skill.materialize(this.#payload.execution), - signal, - ); - const nativeSkillAliases = await raceWithAbort( - exposeNativeSkillAliases(this.#payload.execution, context.logger, materializedSkills), - signal, - ); - const bootstrapArtifacts = await raceWithAbort( - writeSkillBootstrapArtifacts(this.#payload.execution), - signal, - ); - const launch = (this.#agentLaunch ??= { - args: readFallbackArgs(), - command: readFallbackCommand(), - }); - this.#nativeInstructionPath = isOpenCodeCommand(launch.command) - ? await raceWithAbort(writeNativeRuntimeSystemPrompt(this.#payload.execution), signal) - : null; + this.#hostSnapshot = hostSnapshot; + const materializedSkills = await context.ports.skill.materialize( + this.#payload.execution, + signal, + ); + const nativeSkillAliases = await exposeNativeSkillAliases( + this.#payload.execution, + context.logger, + materializedSkills, + signal, + ); + const bootstrapArtifacts = await writeSkillBootstrapArtifacts( + this.#payload.execution, + materializedSkills, + signal, + ); + const launch = (this.#agentLaunch ??= { + args: readFallbackArgs(), + command: readFallbackCommand(), + }); + this.#nativeInstructionPath = isOpenCodeCommand(launch.command) + ? await writeNativeRuntimeSystemPrompt(this.#payload.execution, materializedSkills, signal) + : null; - if (this.#stopRequested || signal.aborted) { - signal.throwIfAborted(); - throw new Error("ACP driver backend stopped during startup."); - } + if (this.#stopRequested || signal.aborted) { + signal.throwIfAborted(); + throw new Error("ACP driver backend stopped during startup."); + } - try { signal.throwIfAborted(); if (this.#stopRequested) { throw new Error("ACP driver backend stopped during startup."); @@ -190,7 +199,6 @@ export class AcpDriverBackend implements AgentDriverBackend { this.#connection?.close( signal.reason instanceof Error ? signal.reason : new Error("ACP startup aborted."), ); - signal.throwIfAborted(); } await this.stop(context, "startup.failed", signal).catch((cleanupError: unknown) => { @@ -198,6 +206,7 @@ export class AcpDriverBackend implements AgentDriverBackend { message: toErrorMessage(cleanupError, "startup cleanup failed"), }); }); + signal.throwIfAborted(); throw error; } } @@ -216,17 +225,19 @@ export class AcpDriverBackend implements AgentDriverBackend { this.#nativeInstructionPath === null ? this.#childProcessEnv : appendOpenCodeInstruction(this.#childProcessEnv, this.#nativeInstructionPath); - const agentProcess = await startAcpAgentProcess( + const startedProcess = await startAcpAgentProcess( context, this.#payload, processEnv, signal, launch, ); + const agentProcess = startedProcess.process; this.#agentProcess = agentProcess; let connection: ClientConnection | null = null; try { + await startedProcess.ready; signal.throwIfAborted(); if (this.#stopRequested) { throw new Error("ACP driver backend stopped while connecting."); @@ -297,16 +308,21 @@ export class AcpDriverBackend implements AgentDriverBackend { const error = reason instanceof Error ? reason : new Error("ACP transport closed unexpectedly."); context.logger.error("driver.acp.transport.failed", error, {}); - const cleanup = stopAcpAgentProcess(context, agentProcess, "connection.failed"); - if (this.#turnController.failActive(error, cleanup)) { + this.#connection = null; + const cleanup = this.#stopOwnedAgent(context, agentProcess, "connection.failed"); + const turnSettled = this.#turnController.routeFatal(error, cleanup); + if (turnSettled === null) { return; } - void cleanup.then( - () => context.lifecycle.fail(error), - (cleanupError: unknown) => - context.lifecycle.fail( - new AggregateError([error, cleanupError], "ACP provider failure cleanup failed."), - ), + void Promise.allSettled([cleanup, turnSettled]).then(([cleanupResult]) => + context.lifecycle.fail( + cleanupResult?.status === "rejected" + ? new AggregateError( + [error, cleanupResult.reason], + "ACP provider failure cleanup failed.", + ) + : error, + ), ); }); @@ -393,12 +409,9 @@ export class AcpDriverBackend implements AgentDriverBackend { this.#connection = null; } connection?.close(error instanceof Error ? error : new Error("ACP connection failed.")); - if (this.#agentProcess === agentProcess) { - this.#agentProcess = null; - } try { - await stopAcpAgentProcess( + await this.#stopOwnedAgent( context, agentProcess, "connection.failed", @@ -444,9 +457,21 @@ export class AcpDriverBackend implements AgentDriverBackend { if (this.#stopTask !== null) { return this.#stopTask; } + if (this.#stopSucceeded) { + return Promise.resolve(); + } - const task = Promise.resolve() + const operation = Promise.resolve() .then(() => this.#performStop(context, reason, signal)) + .finally(() => this.#clientRequests.closePathScope()); + const task = operation + .then(() => { + this.#stopSucceeded = true; + }) + .catch((error: unknown) => { + this.#stopFailure ??= { error }; + throw error; + }) .finally(() => { if (this.#stopTask === task) { this.#stopTask = null; @@ -456,9 +481,15 @@ export class AcpDriverBackend implements AgentDriverBackend { return task; } - async #recycleCancelledTurn(context: AgentDriverContext): Promise { + async #settleCancelledTurn( + context: AgentDriverContext, + providerPromptAdmitted: boolean, + ): Promise { if (this.#stopRequested) { - await this.#stopTask; + await this.#joinStop(); + return; + } + if (!providerPromptAdmitted) { return; } @@ -471,12 +502,11 @@ export class AcpDriverBackend implements AgentDriverBackend { const deadline = Date.now() + ACP_RECYCLE_BUDGET_MS; this.#connection = null; - this.#agentProcess = null; transport.close(new Error("ACP cancelled turn recycling provider process.")); - await stopAcpAgentProcess(context, agentProcess, "turn.cancelled.recycle", deadline); + await this.#stopOwnedAgent(context, agentProcess, "turn.cancelled.recycle", deadline); if (this.#stopRequested) { - await this.#stopTask; + await this.#joinStop(); return; } @@ -491,12 +521,12 @@ export class AcpDriverBackend implements AgentDriverBackend { if (!this.#stopRequested) { throw error; } - await this.#stopTask; + await this.#joinStop(); return; } if (this.#stopRequested) { - await this.#stopTask; + await this.#joinStop(); return; } @@ -582,11 +612,7 @@ export class AcpDriverBackend implements AgentDriverBackend { try { if (agentProcess !== null) { - await stopAcpAgentProcess(context, agentProcess, reason, deadline, signal); - } - - if (this.#agentProcess === agentProcess) { - this.#agentProcess = null; + await this.#stopOwnedAgent(context, agentProcess, reason, deadline, signal); } } catch (error) { processFailure = { error }; @@ -710,6 +736,51 @@ export class AcpDriverBackend implements AgentDriverBackend { return this.#hostSnapshot; } + async #joinStop(): Promise { + if (this.#stopFailure !== null) { + throw this.#stopFailure.error; + } + if (this.#stopTask !== null) { + await this.#stopTask; + return; + } + if (this.#stopSucceeded) { + return; + } + throw new Error("ACP driver stop was requested without an owned cleanup barrier."); + } + + async #stopOwnedAgent( + context: AgentDriverContext, + agentProcess: AcpAgentProcess, + reason: string, + deadline?: number, + signal?: AbortSignal, + ): Promise { + const existing = this.#agentProcessStop; + if (existing?.process === agentProcess) { + await existing.task; + return; + } + + const cleanup = + deadline === undefined + ? stopAcpAgentProcess(context, agentProcess, reason) + : stopAcpAgentProcess(context, agentProcess, reason, deadline, signal); + this.#agentProcessStop = { process: agentProcess, task: cleanup }; + + try { + await cleanup; + if (this.#agentProcess === agentProcess) { + this.#agentProcess = null; + } + } finally { + if (this.#agentProcessStop?.task === cleanup) { + this.#agentProcessStop = null; + } + } + } + #remainingStopMs(deadline: number): number { return Math.max(0, deadline - Date.now()); } diff --git a/src/runtimes/acp/acp-file-system.ts b/src/runtimes/acp/acp-file-system.ts index 4160b76..3f2266c 100644 --- a/src/runtimes/acp/acp-file-system.ts +++ b/src/runtimes/acp/acp-file-system.ts @@ -1,8 +1,11 @@ -import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; -import { dirname } from "node:path"; +import { randomUUID } from "node:crypto"; +import { constants } from "node:fs"; +import type { FileHandle } from "node:fs/promises"; +import { lstat, open, rename, rm } from "node:fs/promises"; +import { join } from "node:path"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; -import { isRecord, raceWithAbort, readNonEmptyString, readNumber } from "./acp-types"; +import { isRecord, readNonEmptyString, readNumber } from "./acp-types"; import { AcpPathScope } from "./acp-path-scope"; interface AcpFileSystemOptions { @@ -12,6 +15,7 @@ interface AcpFileSystemOptions { } const MAX_ACP_FILE_BYTES = 8 * 1_024 * 1_024; +const FILE_CHUNK_BYTES = 64 * 1_024; export class AcpFileSystem { readonly #pathScope: AcpPathScope; @@ -29,19 +33,25 @@ export class AcpFileSystem { throw new Error("ACP fs/read_text_file requires a path."); } - const path = await this.#pathScope.resolveExisting(requestedPath, "ACP file path"); - const file = await stat(path); - signal?.throwIfAborted(); + const path = await this.#pathScope.openFile(requestedPath, "ACP file path"); + let raw: string; - if (!file.isFile()) { - throw new Error("ACP fs/read_text_file requires a regular file."); - } + try { + const metadata = await path.file.stat(); + signal?.throwIfAborted(); - if (file.size > MAX_ACP_FILE_BYTES) { - throw new Error(`ACP file exceeds ${MAX_ACP_FILE_BYTES} bytes.`); - } + if (!metadata.isFile()) { + throw new Error("ACP fs/read_text_file requires a regular file."); + } - const raw = await readFile(path, { encoding: "utf8", signal }); + if (metadata.size > MAX_ACP_FILE_BYTES) { + throw new Error(`ACP file exceeds ${MAX_ACP_FILE_BYTES} bytes.`); + } + + raw = await readBoundedText(path.file, signal); + } finally { + await path.file.close(); + } const line = readNumber(record, "line"); const limit = readNumber(record, "limit"); @@ -76,19 +86,107 @@ export class AcpFileSystem { throw new Error(`ACP file exceeds ${MAX_ACP_FILE_BYTES} bytes.`); } - const path = await this.#pathScope.resolveWritable(requestedPath, "ACP file path"); - await mkdir(dirname(path), { recursive: true }); - signal?.throwIfAborted(); - await writeFile(path, content, { encoding: "utf8", signal }); - await raceWithAbort( - context.ports.file.reportChanged({ + const path = await this.#pathScope.openWritable(requestedPath, "ACP file path"); + const temporaryPath = join(path.directory.procPath, `.${randomUUID()}.tmp`); + const destinationPath = join(path.directory.procPath, path.name); + let temporaryCreated = false; + let committed = false; + + try { + const mode = await readExistingMode(destinationPath); + const temporary = await open( + temporaryPath, + constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, + 0o600, + ); + temporaryCreated = true; + + try { + const bytes = Buffer.from(content, "utf8"); + let offset = 0; + + while (offset < bytes.length) { + signal?.throwIfAborted(); + const { bytesWritten } = await temporary.write( + bytes, + offset, + Math.min(FILE_CHUNK_BYTES, bytes.length - offset), + offset, + ); + + if (bytesWritten === 0) { + throw new Error("ACP fs/write_text_file could not make write progress."); + } + offset += bytesWritten; + } + + await temporary.chmod(mode); + signal?.throwIfAborted(); + await temporary.sync(); + signal?.throwIfAborted(); + await rename(temporaryPath, destinationPath); + committed = true; + await path.directory.file.sync(); + } finally { + await temporary.close(); + } + + const changedPath = join(await this.#pathScope.identify(path.directory), path.name); + await context.ports.file.reportChanged({ change: "upsert", - path, + path: changedPath, reason: "acp.fs", - }), - signal, - ); + }); + } finally { + try { + if (temporaryCreated && !committed) { + await rm(temporaryPath, { force: true }); + } + } finally { + await path.directory.file.close(); + } + } return {}; } } + +async function readExistingMode(path: string): Promise { + try { + const metadata = await lstat(path); + return metadata.isFile() ? metadata.mode & 0o7777 : 0o600; + } catch (error) { + if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") { + return 0o600; + } + throw error; + } +} + +async function readBoundedText(file: FileHandle, signal?: AbortSignal): Promise { + const decoder = new TextDecoder(); + const chunk = Buffer.allocUnsafe(FILE_CHUNK_BYTES); + const content: string[] = []; + let offset = 0; + + for (;;) { + signal?.throwIfAborted(); + const remaining = MAX_ACP_FILE_BYTES + 1 - offset; + + if (remaining <= 0) { + throw new Error(`ACP file exceeds ${MAX_ACP_FILE_BYTES} bytes.`); + } + + const { bytesRead } = await file.read(chunk, 0, Math.min(chunk.length, remaining), offset); + if (bytesRead === 0) { + content.push(decoder.decode()); + return content.join(""); + } + + offset += bytesRead; + if (offset > MAX_ACP_FILE_BYTES) { + throw new Error(`ACP file exceeds ${MAX_ACP_FILE_BYTES} bytes.`); + } + content.push(decoder.decode(chunk.subarray(0, bytesRead), { stream: true })); + } +} diff --git a/src/runtimes/acp/acp-lifecycle-events.ts b/src/runtimes/acp/acp-lifecycle-events.ts index fa6aa9a..b3f96dc 100644 --- a/src/runtimes/acp/acp-lifecycle-events.ts +++ b/src/runtimes/acp/acp-lifecycle-events.ts @@ -130,7 +130,7 @@ export function toAuthEvent(input: { ])[0]!; } -export function shouldIgnoreReplay(params: unknown): boolean { +export function isTurnScopedSessionUpdate(params: unknown): boolean { const record = isRecord(params) ? params : {}; const update = readRecord(record, "update"); @@ -171,12 +171,14 @@ export function normalizePromptUsage(raw: unknown): JsonObject | null { return null; } + // Prompt usage is emitted in the same lossless batch as every open-item + // closure. Keep only the bounded canonical counters; ACP extension metadata + // is arbitrary JSON and cannot safely participate in that atomic batch. return { ...(cachedReadTokens === null ? {} : { cachedReadTokens }), ...(cachedWriteTokens === null ? {} : { cachedWriteTokens }), ...(inputTokens === null ? {} : { inputTokens }), ...(outputTokens === null ? {} : { outputTokens }), - raw, source: "prompt_response", ...(thoughtTokens === null ? {} : { thoughtTokens }), ...(totalTokens === null ? {} : { totalTokens }), diff --git a/src/runtimes/acp/acp-path-scope.ts b/src/runtimes/acp/acp-path-scope.ts index 25021fd..6a3a4b1 100644 --- a/src/runtimes/acp/acp-path-scope.ts +++ b/src/runtimes/acp/acp-path-scope.ts @@ -1,78 +1,295 @@ -import { lstat, realpath } from "node:fs/promises"; -import { dirname, isAbsolute, relative, resolve } from "node:path"; +import { constants } from "node:fs"; +import type { FileHandle } from "node:fs/promises"; +import { mkdir, open, realpath } from "node:fs/promises"; +import { isAbsolute, join, relative, resolve } from "node:path"; export interface AcpPathScopeOptions { readonly allowedRoots: readonly string[]; readonly cwd: string; } +export interface AcpPathHandle { + readonly file: FileHandle; + readonly procPath: string; +} + +export interface AcpWritablePath { + readonly directory: AcpPathHandle; + readonly name: string; +} + +interface AcpRootHandle extends AcpPathHandle { + readonly lexical: string; +} + function contains(root: string, candidate: string): boolean { const path = relative(root, candidate); return path === "" || (!path.startsWith("..") && !isAbsolute(path)); } +function hasCode(error: unknown, code: string): boolean { + return typeof error === "object" && error !== null && "code" in error && error.code === code; +} + +function procPath(file: FileHandle): string { + return `/proc/${process.pid}/fd/${file.fd}`; +} + +async function closeRoots(roots: readonly AcpRootHandle[]): Promise { + const results = await Promise.allSettled(roots.map((root) => root.file.close())); + const failures = results.flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + + if (failures.length > 0) { + throw new AggregateError(failures, "ACP path root capabilities failed to close."); + } +} + export class AcpPathScope { + readonly #configuredRoots: readonly string[]; readonly #cwd: string; - readonly #roots: readonly string[]; - #realRoots: Promise | null = null; + #closed = false; + #closeTask: Promise | null = null; + #initializeTask: Promise | null = null; + #roots: readonly AcpRootHandle[] | null = null; constructor(options: AcpPathScopeOptions) { + if (process.platform !== "linux") { + throw new Error("ACP filesystem capabilities require Linux /proc support."); + } + this.#cwd = resolve(options.cwd); - this.#roots = [options.cwd, ...options.allowedRoots].map((root) => resolve(options.cwd, root)); + this.#configuredRoots = [ + ...new Set([options.cwd, ...options.allowedRoots].map((root) => resolve(options.cwd, root))), + ].sort((left, right) => right.length - left.length); } cwd(): string { return this.#cwd; } - async resolveExisting(path: string, label: string): Promise { - const lexical = this.#resolveLexical(path, label); - const canonical = await realpath(lexical); - await this.#assertCanonical(canonical, path, label); - return canonical; + initialize(): Promise { + if (this.#closed) { + return Promise.reject(new Error("ACP path scope is closed.")); + } + if (this.#roots !== null) { + return Promise.resolve(); + } + if (this.#initializeTask !== null) { + return this.#initializeTask; + } + + const task = this.#acquireRoots().finally(() => { + if (this.#initializeTask === task) { + this.#initializeTask = null; + } + }); + this.#initializeTask = task; + return task; } - async resolveWritable(path: string, label: string): Promise { - const lexical = this.#resolveLexical(path, label); - let ancestor = lexical; + close(): Promise { + this.#closed = true; + if (this.#closeTask !== null) { + return this.#closeTask; + } - for (;;) { - try { - await lstat(ancestor); - break; - } catch (error) { - const parent = dirname(ancestor); - if (parent === ancestor) { - throw error; + const task = this.#close().finally(() => { + if (this.#closeTask === task) { + this.#closeTask = null; + } + }); + this.#closeTask = task; + return task; + } + + identify(handle: AcpPathHandle): Promise { + return realpath(handle.procPath); + } + + async openFile(path: string, label: string): Promise { + const resolved = await this.#resolve(path, label); + return this.#openAt( + resolved.root, + join(resolved.root.procPath, resolved.relativePath), + constants.O_RDONLY | constants.O_NONBLOCK, + path, + label, + ); + } + + async openDirectory(path: string, label: string): Promise { + const resolved = await this.#resolve(path, label); + return this.#openAt( + resolved.root, + join(resolved.root.procPath, resolved.relativePath), + constants.O_RDONLY | constants.O_DIRECTORY, + path, + label, + ); + } + + async openWritable(path: string, label: string): Promise { + const resolved = await this.#resolve(path, label); + const segments = resolved.relativePath.split("/").filter((segment) => segment.length > 0); + const name = segments.pop(); + + if (name === undefined) { + throw new Error(`${label} must name a file: ${path}.`); + } + + let directory = await this.#openAt( + resolved.root, + resolved.root.procPath, + constants.O_RDONLY | constants.O_DIRECTORY, + path, + label, + ); + + try { + for (const segment of segments) { + const child = join(directory.procPath, segment); + let next: AcpPathHandle; + + try { + next = await this.#openAt( + resolved.root, + child, + constants.O_RDONLY | constants.O_DIRECTORY, + path, + label, + ); + } catch (error) { + if (!hasCode(error, "ENOENT")) { + throw error; + } + + try { + await mkdir(child); + } catch (mkdirError) { + if (!hasCode(mkdirError, "EEXIST")) { + throw mkdirError; + } + } + next = await this.#openAt( + resolved.root, + child, + constants.O_RDONLY | constants.O_DIRECTORY, + path, + label, + ); + } + + await directory.file.close(); + directory = next; + } + + return { directory, name }; + } catch (error) { + await directory.file.close().catch(() => {}); + throw error; + } + } + + async #acquireRoots(): Promise { + const roots: AcpRootHandle[] = []; + + try { + for (const lexical of this.#configuredRoots) { + const file = await open(lexical, constants.O_RDONLY | constants.O_DIRECTORY); + const root = { file, lexical, procPath: procPath(file) }; + roots.push(root); + await realpath(root.procPath); + + if (this.#closed) { + throw new Error("ACP path scope closed during initialization."); } - ancestor = parent; } + + if (this.#closed) { + throw new Error("ACP path scope closed during initialization."); + } + this.#roots = roots; + } catch (error) { + try { + await closeRoots(roots); + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + "ACP path root capability initialization failed.", + ); + } + throw error; + } + } + + async #close(): Promise { + await this.#initializeTask?.catch(() => {}); + const roots = this.#roots; + + if (roots === null) { + return; } - const canonicalAncestor = await realpath(ancestor); - await this.#assertCanonical(canonicalAncestor, path, label); - return resolve(canonicalAncestor, relative(ancestor, lexical)); + await closeRoots(roots); + if (this.#roots === roots) { + this.#roots = null; + } } - #resolveLexical(path: string, label: string): string { + async #resolve( + path: string, + label: string, + ): Promise<{ readonly relativePath: string; readonly root: AcpRootHandle }> { if (!isAbsolute(path)) { throw new Error(`${label} must be absolute: ${path}.`); } const candidate = resolve(this.#cwd, path); - if (this.#roots.some((root) => contains(root, candidate))) { - return candidate; + const lexicalRoot = this.#configuredRoots.find((root) => contains(root, candidate)); + + if (lexicalRoot === undefined) { + throw new Error(`${label} is outside the allowed roots: ${path}.`); + } + + await this.initialize(); + const root = this.#roots?.find((entry) => entry.lexical === lexicalRoot); + + if (root === undefined) { + throw new Error("ACP path scope is not initialized."); } - throw new Error(`${label} is outside the allowed roots: ${path}.`); + return { + relativePath: relative(root.lexical, candidate), + root, + }; } - async #assertCanonical(candidate: string, requested: string, label: string): Promise { - const roots = await (this.#realRoots ??= Promise.all( - this.#roots.map((root) => realpath(root)), - )); - if (!roots.some((root) => contains(root, candidate))) { - throw new Error(`${label} resolves outside the allowed roots: ${requested}.`); + async #openAt( + root: AcpRootHandle, + path: string, + flags: number, + requested: string, + label: string, + ): Promise { + const file = await open(path, flags); + const handle = { file, procPath: procPath(file) }; + + try { + const [canonicalRoot, canonical] = await Promise.all([ + realpath(root.procPath), + realpath(handle.procPath), + ]); + + if (!contains(canonicalRoot, canonical)) { + throw new Error(`${label} resolves outside the allowed roots: ${requested}.`); + } + + return handle; + } catch (error) { + await file.close().catch(() => {}); + throw error; } } } diff --git a/src/runtimes/acp/acp-session-events.ts b/src/runtimes/acp/acp-session-events.ts index e3a7fe0..7035699 100644 --- a/src/runtimes/acp/acp-session-events.ts +++ b/src/runtimes/acp/acp-session-events.ts @@ -1,6 +1,6 @@ export { normalizePromptUsage, - shouldIgnoreReplay, + isTurnScopedSessionUpdate, toAuthEvent, toInitializeEvents, toPromptStartEvents, diff --git a/src/runtimes/acp/acp-terminal-manager.ts b/src/runtimes/acp/acp-terminal-manager.ts index 390c2ed..6aa6c2e 100644 --- a/src/runtimes/acp/acp-terminal-manager.ts +++ b/src/runtimes/acp/acp-terminal-manager.ts @@ -172,26 +172,35 @@ export class AcpTerminalManager { const args = readArray(record, "args").filter( (entry): entry is string => typeof entry === "string", ); - const cwd = await this.#pathScope.resolveExisting( - readNonEmptyString(record, "cwd") ?? this.#pathScope.cwd(), - "ACP terminal cwd", - ); + const requestedCwd = readNonEmptyString(record, "cwd") ?? this.#pathScope.cwd(); const requestedOutputByteLimit = readNumber(record, "outputByteLimit"); const outputByteLimit = normalizeByteLimit(requestedOutputByteLimit); const env = this.#readTerminalEnv(record); const terminalId = createDriverId(); - const processTree = createProcessTreeEnvironment({ - ...this.#env, - ...env, - }); - const child = spawn(command, args, { - cwd, - detached: true, - env: processTree.env, - stdio: ["pipe", "pipe", "pipe"], - }); + const cwd = await this.#pathScope.openDirectory(requestedCwd, "ACP terminal cwd"); + let processTree: ReturnType; + let child: ChildProcessWithoutNullStreams; + let cwdPath: string; + + try { + signal?.throwIfAborted(); + processTree = createProcessTreeEnvironment({ + ...this.#env, + ...env, + }); + child = spawn(command, args, { + cwd: cwd.procPath, + detached: true, + env: processTree.env, + stdio: ["pipe", "pipe", "pipe"], + }); + } catch (error) { + await cwd.file.close().catch(() => {}); + throw error; + } const target = bindSpawnedProcess(child, process.platform, processTree); const spawned = once(child, "spawn"); + void spawned.catch(() => {}); const { promise: closed, resolve: resolveClosed } = Promise.withResolvers(); const { promise: exited, @@ -287,6 +296,8 @@ export class AcpTerminalManager { ); } await raceWithAbort(spawned, signal); + cwdPath = await this.#pathScope.identify(cwd); + await cwd.file.close(); if (process.platform === "linux" && terminal.watchdog === null) { throw new Error(`ACP terminal ${terminalId} supervision could not start.`); } @@ -309,7 +320,7 @@ export class AcpTerminalManager { kind: "terminal.created", payload: { command, - cwd, + cwd: cwdPath, outputByteLimit, terminalId, }, @@ -357,6 +368,8 @@ export class AcpTerminalManager { releaseLinuxProcessMarker(terminal.marker); this.#removeTerminal(terminal); throw error; + } finally { + await cwd.file.close().catch(() => {}); } return { terminalId }; @@ -431,7 +444,11 @@ export class AcpTerminalManager { async stopAll(context: AgentDriverContext): Promise { this.#stopping = true; - await this.#stop(context); + try { + await this.#stop(context); + } finally { + await this.#pathScope.close(); + } } async stopTurn(context: AgentDriverContext, turn: number): Promise { diff --git a/src/runtimes/acp/acp-tool-events.ts b/src/runtimes/acp/acp-tool-events.ts index 76ff306..4b37aa8 100644 --- a/src/runtimes/acp/acp-tool-events.ts +++ b/src/runtimes/acp/acp-tool-events.ts @@ -15,6 +15,19 @@ import type { JsonObject } from "./acp-types"; export type RuntimeToolStatus = "cancelled" | "completed" | "failed" | "running"; +// Completed calls are retained only to suppress bounded late replays. They do +// not participate in terminal settlement admission: only open calls can emit +// terminal closures. The cache has its own independent memory bound. +const MAX_ACP_COMPLETED_TOOL_HISTORY_BYTES = MAX_ACP_LOSSLESS_EVENT_BYTES; +const MAX_ACP_COMPLETED_TOOL_HISTORY_ITEMS = 1_024; + +interface AcpToolState { + readonly completed: boolean; + readonly hasNonzeroExit: boolean; + readonly snapshot: JsonObject; + readonly started: boolean; +} + function readToolDisplayString(value: unknown): string | undefined { const display = stringifyForDisplay(value); @@ -41,37 +54,51 @@ function hasNonzeroExecuteExit(kind: unknown, update: JsonObject | null): boolea } export class AcpToolEventState { - #completed = new Set(); - #nonzeroExecuteExits = new Set(); - #snapshots = new Map(); - #started = new Set(); + #hadActivity = false; + #tools = new Map(); hasActivity(): boolean { - return this.#started.size > 0; + return this.#hadActivity; } hasStarted(toolCallId: string): boolean { - return this.#started.has(toolCallId); + return this.#tools.get(toolCallId)?.started ?? false; + } + + openItemCount(): number { + let count = 0; + + for (const tool of this.#tools.values()) { + if (tool.started && !tool.completed) { + count += 1; + } + } + + return count; + } + + retainedOpenState(): readonly JsonObject[] { + return [...this.#tools.values()].flatMap((tool) => + tool.started && !tool.completed ? [tool.snapshot] : [], + ); + } + + compactHistory(): void { + this.#trimCompletedHistory(); } clear(): void { - this.#completed.clear(); - this.#nonzeroExecuteExits.clear(); - this.#snapshots.clear(); - this.#started.clear(); + this.#hadActivity = false; + this.#tools.clear(); } checkpoint(): () => void { - const completed = new Set(this.#completed); - const nonzeroExecuteExits = new Set(this.#nonzeroExecuteExits); - const snapshots = new Map(this.#snapshots); - const started = new Set(this.#started); + const hadActivity = this.#hadActivity; + const tools = new Map([...this.#tools].map(([toolCallId, tool]) => [toolCallId, { ...tool }])); return () => { - this.#completed = completed; - this.#nonzeroExecuteExits = nonzeroExecuteExits; - this.#snapshots = snapshots; - this.#started = started; + this.#hadActivity = hadActivity; + this.#tools = tools; }; } @@ -81,13 +108,14 @@ export class AcpToolEventState { toolCallId: string; update: JsonObject | null; }): { changed: boolean; payload: JsonObject; status: RuntimeToolStatus } { - const previous = this.#snapshots.get(input.toolCallId); - const previousStatus = previous?.["status"]; - const kind = readNonEmptyString(input.update, "kind") ?? previous?.["kind"] ?? "tool"; + const previous = this.#tools.get(input.toolCallId); + const previousSnapshot = previous?.snapshot; + const previousStatus = previousSnapshot?.["status"]; + const kind = readNonEmptyString(input.update, "kind") ?? previousSnapshot?.["kind"] ?? "tool"; const nextStatus = input.status ?? "running"; const hasNonzeroExit = - this.#nonzeroExecuteExits.has(input.toolCallId) || hasNonzeroExecuteExit(kind, input.update); + (previous?.hasNonzeroExit ?? false) || hasNonzeroExecuteExit(kind, input.update); const status = previousStatus === "cancelled" || @@ -100,12 +128,12 @@ export class AcpToolEventState { // The projection layer links tool calls to their assistant message via // parentMessageId; keep the first observed parent for the call's lifetime. const parentMessageId = - (typeof previous?.["parentMessageId"] === "string" - ? previous["parentMessageId"] + (typeof previousSnapshot?.["parentMessageId"] === "string" + ? previousSnapshot["parentMessageId"] : undefined) ?? input.parentMessageId; - const title = readNonEmptyString(input.update, "title") ?? previous?.["title"]; + const title = readNonEmptyString(input.update, "title") ?? previousSnapshot?.["title"]; const payload = { - ...previous, + ...previousSnapshot, ...toToolCallPayload(input.toolCallId, status, input.update), kind, ...(parentMessageId === undefined ? {} : { parentMessageId }), @@ -113,18 +141,15 @@ export class AcpToolEventState { ...(typeof title === "string" ? { title } : {}), toolCallId: input.toolCallId, }; - const changed = previous === undefined || !isDeepStrictEqual(previous, payload); - - if (Buffer.byteLength(JSON.stringify(payload), "utf8") > MAX_ACP_LOSSLESS_EVENT_BYTES) { - throw new RangeError(`ACP tool update exceeds ${MAX_ACP_LOSSLESS_EVENT_BYTES} UTF-8 bytes.`); - } - - if (hasNonzeroExit) { - this.#nonzeroExecuteExits.add(input.toolCallId); - } - - if (changed) { - this.#snapshots.set(input.toolCallId, structuredClone(payload)); + const changed = previousSnapshot === undefined || !isDeepStrictEqual(previousSnapshot, payload); + + if (changed || hasNonzeroExit !== previous?.hasNonzeroExit) { + this.#tools.set(input.toolCallId, { + completed: previous?.completed ?? false, + hasNonzeroExit, + snapshot: changed ? structuredClone(payload) : previous!.snapshot, + started: previous?.started ?? false, + }); } return { changed, payload, status }; @@ -136,11 +161,18 @@ export class AcpToolEventState { toolCallId: string; update: JsonObject | null; }): DriverEventInput | null { - if (this.#completed.has(input.toolCallId)) { + const tool = this.#tools.get(input.toolCallId); + + if (tool === undefined) { + throw new Error("ACP tool completion requires a projected tool call."); + } + if (tool.completed) { + this.#trimCompletedHistory(); return null; } - this.#completed.add(input.toolCallId); + this.#tools.set(input.toolCallId, { ...tool, completed: true }); + this.#trimCompletedHistory(); return { kind: "item.completed", payload: { @@ -160,12 +192,12 @@ export class AcpToolEventState { }): DriverEventInput[] { const events: DriverEventInput[] = []; - for (const itemId of this.#started) { - if (this.#completed.has(itemId)) { + for (const [itemId, tool] of this.#tools) { + if (!tool.started || tool.completed) { continue; } - this.#completed.add(itemId); + this.#tools.set(itemId, { ...tool, completed: true }); events.push({ kind: "tool.call.updated", payload: { @@ -187,6 +219,8 @@ export class AcpToolEventState { }); } + this.#trimCompletedHistory(); + return events; } @@ -196,11 +230,17 @@ export class AcpToolEventState { title: string; toolCallId: string; }): DriverEventInput[] { - if (this.#started.has(input.toolCallId)) { + const tool = this.#tools.get(input.toolCallId); + + if (tool?.started) { return []; } + if (tool === undefined) { + throw new Error("ACP tool start requires a projected tool call."); + } - this.#started.add(input.toolCallId); + this.#hadActivity = true; + this.#tools.set(input.toolCallId, { ...tool, started: true }); return [ { kind: "item.started", @@ -214,6 +254,29 @@ export class AcpToolEventState { }, ]; } + + #trimCompletedHistory(): void { + const completed = [...this.#tools].filter(([, tool]) => tool.completed); + let retainedItems = completed.length; + let bytes = completed.reduce( + (total, [toolCallId, tool]) => + total + Buffer.byteLength(JSON.stringify([toolCallId, tool.snapshot]), "utf8"), + 0, + ); + + for (const [toolCallId, tool] of completed) { + if ( + retainedItems <= MAX_ACP_COMPLETED_TOOL_HISTORY_ITEMS && + bytes <= MAX_ACP_COMPLETED_TOOL_HISTORY_BYTES + ) { + break; + } + + this.#tools.delete(toolCallId); + retainedItems -= 1; + bytes -= Buffer.byteLength(JSON.stringify([toolCallId, tool.snapshot]), "utf8"); + } + } } export function toRuntimeToolStatus(status: string | null): RuntimeToolStatus { diff --git a/src/runtimes/acp/acp-turn-controller.ts b/src/runtimes/acp/acp-turn-controller.ts index 6d1523a..bc24e1a 100644 --- a/src/runtimes/acp/acp-turn-controller.ts +++ b/src/runtimes/acp/acp-turn-controller.ts @@ -1,5 +1,5 @@ import { methods as acpMethods } from "@agentclientprotocol/sdk"; -import type { ClientContext } from "@agentclientprotocol/sdk"; +import type { ClientContext, StopReason } from "@agentclientprotocol/sdk"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; import { ACTIVE_TURN_CANCEL_GRACE_MS } from "../../core/driver-command-dispatcher"; @@ -29,12 +29,34 @@ interface ActiveAcpTurn { drainDeadline: ReturnType | null; fatal: { readonly cleanup: Promise; readonly error: Error } | null; providerPromptAdmitted: boolean; + promptResponseAccepted: boolean; readonly providerPromptSettled: ReturnType>; readonly runId: RunId; + readonly settled: ReturnType>; readonly terminalTurn: number; terminalStarted: boolean; } +function createActiveTurn(runId: RunId, terminalTurn: number): ActiveAcpTurn { + return { + cancellation: new AbortController(), + cancellationBarrier: null, + cancellationReason: null, + cancellationRequest: null, + cancelRequested: false, + drainCancellation: new AbortController(), + drainDeadline: null, + fatal: null, + providerPromptAdmitted: false, + promptResponseAccepted: false, + providerPromptSettled: Promise.withResolvers(), + runId, + settled: Promise.withResolvers(), + terminalStarted: false, + terminalTurn, + }; +} + class AcpPromptTerminalError extends Error { override readonly name = "AcpPromptTerminalError"; @@ -78,10 +100,6 @@ function requestCancellation(active: ActiveAcpTurn, reason: string): void { active.cancellation.abort(new DriverTurnCancelledError(reason)); } -function readFatal(active: ActiveAcpTurn): ActiveAcpTurn["fatal"] { - return active.fatal; -} - export type AcpTurnEventPush = ( context: AgentDriverContext, reason: string, @@ -95,7 +113,25 @@ export type AcpTurnTerminalPush = ( terminal: DriverEventInput, ) => Promise; -export type AcpCancelledTurnBarrier = (context: AgentDriverContext) => Promise; +export type AcpCancelledTurnBarrier = ( + context: AgentDriverContext, + providerPromptAdmitted: boolean, +) => Promise; + +function parsePromptStopReason(value: unknown): StopReason { + switch (value) { + case "cancelled": + case "end_turn": + case "max_tokens": + case "max_turn_requests": + case "refusal": { + return value; + } + default: { + throw new Error("ACP prompt response contains an invalid stop reason."); + } + } +} export class AcpTurnController { #active: ActiveAcpTurn | null = null; @@ -129,16 +165,19 @@ export class AcpTurnController { } } - failActive(error: Error, cleanup: Promise): boolean { + routeFatal(error: Error, cleanup: Promise): Promise | null { const active = this.#active; - if (active === null || active.terminalStarted) { - return false; + if (active === null) { + return Promise.resolve(); + } + if (active.promptResponseAccepted || active.terminalStarted) { + return active.settled.promise; } active.fatal ??= { cleanup, error }; active.cancellation.abort(error); void cleanup.catch(() => {}); - return true; + return null; } async handleInput( @@ -156,21 +195,7 @@ export class AcpTurnController { } const messageId = createDriverId() as MessageId; - const active = { - cancellation: new AbortController(), - cancellationBarrier: null, - cancellationReason: null, - cancellationRequest: null, - cancelRequested: false, - drainCancellation: new AbortController(), - drainDeadline: null, - fatal: null, - providerPromptAdmitted: false, - providerPromptSettled: Promise.withResolvers(), - runId, - terminalTurn: clientRequests.beginTurnTerminals(), - terminalStarted: false, - }; + const active = createActiveTurn(runId, clientRequests.beginTurnTerminals()); this.#active = active; this.events.begin({ messageId, runId }); const onAbort = () => @@ -185,7 +210,7 @@ export class AcpTurnController { onAbort(); } clientRequests.openPermissionIngress(); - clientRequests.openTurnUpdateIngress(); + clientRequests.openTurnTranscriptIngress(); let drainTask: Promise | null = null; let preserveEventState = false; const drain = async () => { @@ -242,13 +267,14 @@ export class AcpTurnController { if (active.cancelRequested) { clientRequests.closePermissionIngress(); - clientRequests.closeTurnUpdateIngress(); + clientRequests.closeTurnTranscriptIngress(); await drain(); await this.#publishCancellationRequest( context, active, active.cancellationReason ?? "ACP driver backend turn was cancelled.", ); + await this.#crossCancelledTurnBarrier(context, active, clientRequests); active.terminalStarted = true; await publishTerminal("driver.acp.prompt.cancelled", () => this.events.completePrompt("cancelled", null), @@ -265,15 +291,20 @@ export class AcpTurnController { prompt: [{ text: input.text, type: "text" }], sessionId, }); + if (active.fatal !== null) { + throw active.fatal.error; + } + const providerStopReason = parsePromptStopReason(promptResult.stopReason); + active.promptResponseAccepted = true; active.providerPromptSettled.resolve(true); clientRequests.closePermissionIngress(); - clientRequests.closeTurnUpdateIngress(); - if (promptResult.stopReason === "cancelled") { + clientRequests.closeTurnTranscriptIngress(); + if (providerStopReason === "cancelled") { requestCancellation(active, "ACP provider cancelled the turn."); } await drain(); - const stopReason = active.cancelRequested ? "cancelled" : promptResult.stopReason; - const promptCancelled = active.cancelRequested || promptResult.stopReason === "cancelled"; + const stopReason = active.cancelRequested ? "cancelled" : providerStopReason; + const promptCancelled = active.cancelRequested || providerStopReason === "cancelled"; if (promptCancelled) { await this.#crossCancelledTurnBarrier(context, active, clientRequests); } @@ -290,7 +321,7 @@ export class AcpTurnController { const promptFailed = completionEvents.some((event) => event.kind === "run.failed"); context.logger.info( promptFailed ? "driver.acp.prompt.failed" : "driver.acp.prompt.completed", - { sessionId, stopReason: promptResult.stopReason }, + { sessionId, stopReason: providerStopReason }, ); if (promptCancelled) { @@ -303,15 +334,10 @@ export class AcpTurnController { } catch (error) { active.providerPromptSettled.resolve(active.providerPromptAdmitted && active.cancelRequested); clientRequests.closePermissionIngress(); - clientRequests.closeTurnUpdateIngress(); + clientRequests.closeTurnTranscriptIngress(); if (preserveEventState) { throw error; } - const fatal = readFatal(active); - const fatalCleanup = - fatal === null - ? null - : Promise.allSettled([fatal.cleanup, clientRequests.stopTerminals(context)]); let catchDrainError: unknown = null; try { await drain(); @@ -322,23 +348,40 @@ export class AcpTurnController { }); } - if (fatal !== null && fatalCleanup !== null) { - const cleanupResults = await fatalCleanup; - const cleanupFailure = cleanupResults.find((result) => result.status === "rejected"); - if (cleanupFailure?.status === "rejected") { + const fatal = active.fatal; + + if (fatal !== null) { + active.terminalStarted = true; + const cleanupResults = await Promise.allSettled([ + fatal.cleanup, + clientRequests.stopTerminals(context), + ]); + const cleanupFailures = cleanupResults.flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + + try { + await publishTerminal("driver.acp.provider.failed", () => + this.events.failPrompt({ + code: "acp.provider_failed", + message: fatal.error.message, + }), + ); + } catch (terminalError) { + if (cleanupFailures.length > 0) { + throw new AggregateError( + [fatal.error, ...cleanupFailures, terminalError], + "ACP provider failure cleanup and terminal publication failed.", + ); + } + throw terminalError; + } + if (cleanupFailures.length > 0) { throw new AggregateError( - [fatal.error, cleanupFailure.reason], + [fatal.error, ...cleanupFailures], "ACP provider failure cleanup failed.", ); } - - active.terminalStarted = true; - await publishTerminal("driver.acp.provider.failed", () => - this.events.failPrompt({ - code: "acp.provider_failed", - message: fatal.error.message, - }), - ); throw fatal.error; } @@ -398,7 +441,7 @@ export class AcpTurnController { throw error; } finally { clientRequests.closePermissionIngress(); - clientRequests.closeTurnUpdateIngress(); + clientRequests.closeTurnTranscriptIngress(); if (active.drainDeadline !== null) { clearTimeout(active.drainDeadline); } @@ -408,6 +451,7 @@ export class AcpTurnController { if (!preserveEventState) { this.events.clear(); } + active.settled.resolve(); } } @@ -485,9 +529,7 @@ export class AcpTurnController { try { await (active.cancellationBarrier ??= (async () => { await clientRequests.stopTurnTerminals(context, active.terminalTurn); - if (active.providerPromptAdmitted) { - await this.#cancelledTurnBarrier(context); - } + await this.#cancelledTurnBarrier(context, active.providerPromptAdmitted); })()); } catch (error) { throw new DriverTurnCancellationCleanupError( diff --git a/src/runtimes/atomic-file.ts b/src/runtimes/atomic-file.ts new file mode 100644 index 0000000..771d14b --- /dev/null +++ b/src/runtimes/atomic-file.ts @@ -0,0 +1,293 @@ +import { randomUUID } from "node:crypto"; +import { constants, type Dirent } from "node:fs"; +import { lstat, mkdir, open, opendir, rename, unlink } from "node:fs/promises"; +import type { FileHandle } from "node:fs/promises"; +import { basename, join, resolve } from "node:path"; + +function hasErrorCode(error: unknown, code: string): boolean { + return typeof error === "object" && error !== null && "code" in error && error.code === code; +} + +const activeAtomicWriteTemporaryFiles = new Set(); +const ATOMIC_WRITE_TEMPORARY_ID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const MAX_ATOMIC_WRITE_TEMPORARY_SCAN_ENTRIES = 256; + +export async function closeFileHandles( + handles: readonly (FileHandle | null | undefined)[], +): Promise { + const results = await Promise.allSettled( + handles + .filter((handle): handle is FileHandle => handle !== null && handle !== undefined) + .map((handle) => handle.close()), + ); + return results.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])); +} + +async function closeFileHandlesAndThrow( + error: unknown, + handles: readonly (FileHandle | null | undefined)[], + message: string, +): Promise { + const closeFailures = await closeFileHandles(handles); + throw closeFailures.length > 0 ? new AggregateError([error, ...closeFailures], message) : error; +} + +async function atomicWriteTemporaryFileKey(directory: FileHandle, name: string): Promise { + const stats = await directory.stat(); + return `${String(stats.dev)}:${String(stats.ino)}:${name}`; +} + +export function directoryEntryPath(directory: FileHandle, name: string): string { + if (name.length === 0 || name === "." || name === ".." || basename(name) !== name) { + throw new Error(`Directory entry name is invalid: ${name}.`); + } + + return join(openedDirectoryPath(directory), name); +} + +export function openedDirectoryPath(directory: FileHandle): string { + return join("/proc/self/fd", String(directory.fd)); +} + +export async function openRealDirectory(path: string, label: string): Promise { + if (process.platform !== "linux") { + throw new Error(`${label} requires Linux /proc filesystem capabilities.`); + } + + let directory: FileHandle; + + try { + directory = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW); + } catch (error) { + if (hasErrorCode(error, "ELOOP") || hasErrorCode(error, "ENOTDIR")) { + throw new Error(`${label} must be a real directory: ${path}.`, { cause: error }); + } + throw error; + } + + try { + if (!(await directory.stat()).isDirectory()) { + throw new Error(`${label} must be a real directory: ${path}.`); + } + return directory; + } catch (error) { + return closeFileHandlesAndThrow(error, [directory], `Failed to open ${path}.`); + } +} + +export async function ensureRealDirectoryAt( + parent: FileHandle, + name: string, + label: string, + signal?: AbortSignal, +): Promise { + signal?.throwIfAborted(); + const path = directoryEntryPath(parent, name); + let created = false; + + try { + await mkdir(path); + created = true; + } catch (error) { + if (!hasErrorCode(error, "EEXIST")) { + throw error; + } + } + + const directory = await openRealDirectory(path, label); + try { + if (created) { + await parent.sync(); + } + signal?.throwIfAborted(); + return directory; + } catch (error) { + const closeFailures = await closeFileHandles([directory]); + if (closeFailures.length > 0) { + throw new AggregateError([error, ...closeFailures], `Failed to create ${path}.`); + } + throw error; + } +} + +async function walkAbsoluteRealDirectory( + path: string, + label: string, + create: boolean, + signal?: AbortSignal, +): Promise { + signal?.throwIfAborted(); + const absolutePath = resolve(path); + let directory = await openRealDirectory("/", label); + + for (const segment of absolutePath.split("/").filter(Boolean)) { + let next: FileHandle; + try { + signal?.throwIfAborted(); + next = create + ? await ensureRealDirectoryAt(directory, segment, label, signal) + : await openRealDirectory(directoryEntryPath(directory, segment), label); + } catch (error) { + return closeFileHandlesAndThrow(error, [directory], `Failed to walk ${absolutePath}.`); + } + + const closeFailures = await closeFileHandles([directory]); + if (closeFailures.length > 0) { + return closeFileHandlesAndThrow( + new AggregateError(closeFailures, `Failed to close an ancestor of ${absolutePath}.`), + [next], + `Failed to walk ${absolutePath}.`, + ); + } + directory = next; + } + + return directory; +} + +export function openAbsoluteRealDirectory(path: string, label: string): Promise { + return walkAbsoluteRealDirectory(path, label, false); +} + +export function ensureAbsoluteRealDirectory( + path: string, + label: string, + signal?: AbortSignal, +): Promise { + return walkAbsoluteRealDirectory(path, label, true, signal); +} + +export async function assertDirectoryIdentity( + directory: FileHandle, + path: string, + label: string, +): Promise { + const openedStats = await directory.stat(); + let pathStats: Awaited> | null; + + try { + pathStats = await lstat(path); + } catch (error) { + if (!hasErrorCode(error, "ENOENT")) { + throw error; + } + pathStats = null; + } + + if ( + pathStats === null || + pathStats.isSymbolicLink() || + !pathStats.isDirectory() || + pathStats.dev !== openedStats.dev || + pathStats.ino !== openedStats.ino + ) { + throw new Error(`${label} changed while managed files were being written: ${path}.`); + } +} + +export async function isActiveAtomicWriteTemporaryFile( + directory: FileHandle, + name: string, +): Promise { + return activeAtomicWriteTemporaryFiles.has(await atomicWriteTemporaryFileKey(directory, name)); +} + +export async function cleanupAtomicWriteTemporaryFiles( + directory: FileHandle, + targetNames: readonly string[], + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + await using entries = await opendir(openedDirectoryPath(directory)); + let removed = false; + let scanned = 0; + + for await (const entry of entries) { + signal.throwIfAborted(); + scanned += 1; + if (scanned > MAX_ATOMIC_WRITE_TEMPORARY_SCAN_ENTRIES) { + break; + } + + const target = targetNames.find( + (name) => + entry.name.startsWith(`.${name}.`) && + entry.name.endsWith(".tmp") && + ATOMIC_WRITE_TEMPORARY_ID_PATTERN.test(entry.name.slice(name.length + 2, -".tmp".length)), + ); + if (target === undefined || (await isActiveAtomicWriteTemporaryFile(directory, entry.name))) { + continue; + } + if (entry.isDirectory() && !entry.isSymbolicLink()) { + throw new Error(`Atomic write temporary path must not be a directory: ${entry.name}.`); + } + + await unlink(directoryEntryPath(directory, entry.name)); + removed = true; + } + + if (removed) { + await directory.sync(); + } +} + +export async function readDirectoryEntriesBounded( + directory: FileHandle, + label: string, + maxEntries: number, + signal?: AbortSignal, +): Promise { + await using stream = await opendir(openedDirectoryPath(directory)); + const entries: Dirent[] = []; + + for await (const entry of stream) { + signal?.throwIfAborted(); + if (entries.length >= maxEntries) { + throw new Error(`${label} contains too many entries.`); + } + entries.push(entry); + } + + return entries.toSorted((a, b) => a.name.localeCompare(b.name)); +} + +export async function writeFileAtomically( + directory: FileHandle, + name: string, + contents: string, + mode: number, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + const path = directoryEntryPath(directory, name); + const temporaryName = `.${name}.${randomUUID()}.tmp`; + const temporaryPath = directoryEntryPath(directory, temporaryName); + const temporaryKey = await atomicWriteTemporaryFileKey(directory, temporaryName); + let temporaryFileCreated = false; + activeAtomicWriteTemporaryFiles.add(temporaryKey); + + try { + await using temporaryFile = await open(temporaryPath, "wx", mode); + temporaryFileCreated = true; + await temporaryFile.writeFile(contents, { encoding: "utf8", signal }); + await temporaryFile.sync(); + + signal.throwIfAborted(); + await rename(temporaryPath, path); + await directory.sync(); + } catch (error) { + if (temporaryFileCreated) { + try { + await unlink(temporaryPath); + } catch (cleanupError) { + if (!hasErrorCode(cleanupError, "ENOENT")) { + throw new AggregateError([error, cleanupError], `Failed to clean ${temporaryPath}.`); + } + } + } + throw error; + } finally { + activeAtomicWriteTemporaryFiles.delete(temporaryKey); + } +} diff --git a/src/runtimes/child-process-env.ts b/src/runtimes/child-process-env.ts index 6d01339..37e7403 100644 --- a/src/runtimes/child-process-env.ts +++ b/src/runtimes/child-process-env.ts @@ -1,3 +1,5 @@ +import { delimiter } from "node:path"; + import { DRIVER_BOOT_PAYLOAD_ENV_NAME, DRIVER_BOOT_PAYLOAD_FILE_ENV_NAME } from "../protocol/boot"; import type { DriverExecutionEnvironment } from "../protocol/boot"; @@ -6,7 +8,9 @@ export { DRIVER_BOOT_PAYLOAD_ENV_NAME, DRIVER_BOOT_PAYLOAD_FILE_ENV_NAME }; export function buildRuntimeChildProcessEnv( paths: DriverExecutionEnvironment["paths"], env: NodeJS.ProcessEnv, + platform: NodeJS.Platform = process.platform, ): Record { + const pathDelimiter = platform === "win32" ? ";" : delimiter; const childEnv = Object.fromEntries( Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined), ); @@ -17,7 +21,23 @@ export function buildRuntimeChildProcessEnv( ["PYTHONPATH", paths?.python ?? []], ] as const) { if (artifactPaths.length > 0) { - childEnv[name] = [...artifactPaths, ...(childEnv[name] ? [childEnv[name]] : [])].join(":"); + const inheritedKey = + platform === "win32" + ? Object.hasOwn(childEnv, name) + ? name + : Object.keys(childEnv).find((key) => key.toUpperCase() === name) + : name; + const inherited = inheritedKey === undefined ? undefined : childEnv[inheritedKey]; + + if (platform === "win32") { + for (const key of Object.keys(childEnv)) { + if (key.toUpperCase() === name) { + delete childEnv[key]; + } + } + } + + childEnv[name] = [...artifactPaths, ...(inherited ? [inherited] : [])].join(pathDelimiter); } } diff --git a/src/runtimes/child-process.ts b/src/runtimes/child-process.ts index 8374dad..ed5982b 100644 --- a/src/runtimes/child-process.ts +++ b/src/runtimes/child-process.ts @@ -425,9 +425,6 @@ export function createProcessTreeEnvironment( ): ProcessTreeEnvironment { const supervisor = ensureLinuxProcessTreeSupervisor(); const marker = randomUUID(); - if (supervisor !== null) { - linuxMarkedProcessState(marker).cutoff = Number(supervisor.startTime); - } return { env: { ...env, @@ -488,15 +485,16 @@ export function bindSpawnedProcess( ): BoundSpawnedProcess { const pid = child.pid; const stat = platform === "linux" && pid !== undefined ? readLinuxProcessStat(pid) : null; - if (pid !== undefined && stat !== null && processTree !== undefined) { - const state = linuxMarkedProcessStates.get(processTree.marker); - const stdin = linuxProcessTreeSupervisor?.process.stdin; - if (state !== undefined) { - state.cutoff = Number(stat.startTime); + if (platform === "linux" && processTree !== undefined && linuxProcessTreeSupervisor !== null) { + const state = linuxMarkedProcessState(processTree.marker); + state.cutoff = Number(stat?.startTime ?? linuxProcessTreeSupervisor.startTime); + const stdin = linuxProcessTreeSupervisor.process.stdin; + if (pid !== undefined && stat !== null) { state.processes.set(pid, stat.startTime); } if ( - state === undefined || + pid === undefined || + stat === null || stdin === null || stdin === undefined || stdin.destroyed || diff --git a/src/runtimes/claude/agent-sdk-driver-backend.ts b/src/runtimes/claude/agent-sdk-driver-backend.ts index f5f87be..54d715e 100644 --- a/src/runtimes/claude/agent-sdk-driver-backend.ts +++ b/src/runtimes/claude/agent-sdk-driver-backend.ts @@ -116,12 +116,13 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { throw new Error("Claude Agent SDK backend cannot restart after stopping."); } - const materializedSkills = await raceWithAbort( - context.ports.skill.materialize(this.#payload.execution), + const materializedSkills = await context.ports.skill.materialize( + this.#payload.execution, signal, ); - const bootstrapArtifacts = await raceWithAbort( - writeSkillBootstrapArtifacts(this.#payload.execution), + const bootstrapArtifacts = await writeSkillBootstrapArtifacts( + this.#payload.execution, + materializedSkills, signal, ); const { homePath } = this.#payload.execution.session; diff --git a/src/runtimes/claude/agent-sdk-message-translator.ts b/src/runtimes/claude/agent-sdk-message-translator.ts index 8215ab5..3b7bf74 100644 --- a/src/runtimes/claude/agent-sdk-message-translator.ts +++ b/src/runtimes/claude/agent-sdk-message-translator.ts @@ -82,36 +82,18 @@ export class ClaudeAgentSdkMessageTranslator { this.#permissionDenialAdvisories.clear(); } - async endActiveThought(context: AgentDriverContext): Promise { + async #cancelOpenTurn(context: AgentDriverContext): Promise { for (const [messageId, thoughtId] of this.#state.activeThoughts()) { - await this.#events.endThought(context, thoughtId); + await this.#events.cancelThought(context, thoughtId); this.#state.deleteThoughtId(messageId); } - } - - async finishTurn( - context: AgentDriverContext, - toolStatus: "cancelled" | "completed" | "failed", - ): Promise { - if (toolStatus === "cancelled") { - for (const [messageId, thoughtId] of this.#state.activeThoughts()) { - await this.#events.cancelThought(context, thoughtId); - this.#state.deleteThoughtId(messageId); - } - } else { - await this.endActiveThought(context); - } for (const [messageId, runId] of this.#state.assistantMessages()) { - if (toolStatus === "cancelled") { - await this.#events.cancelMessage(context, messageId); - this.#state.completeAssistantMessage(runId, messageId, false); - } else { - await this.#endAssistantMessage(context, runId, messageId); - } + await this.#events.cancelMessage(context, messageId); + this.#state.completeAssistantMessage(runId, messageId, false); } - await this.#events.finishTools(context, toolStatus); + await this.#events.finishTools(context, "cancelled"); const taskClosure = this.#tasks.prepareClosure(); for (const [index, event] of taskClosure.events.entries()) { taskClosure.beforePublish(index); @@ -768,7 +750,7 @@ export class ClaudeAgentSdkMessageTranslator { context: AgentDriverContext, message: Extract, ): Promise { - await this.finishTurn(context, "cancelled"); + await this.#cancelOpenTurn(context); await this.#options.replaceNativeSessionId( context, message.session_id, diff --git a/src/runtimes/claude/agent-sdk-prewarm.ts b/src/runtimes/claude/agent-sdk-prewarm.ts index 0d75524..09c9258 100644 --- a/src/runtimes/claude/agent-sdk-prewarm.ts +++ b/src/runtimes/claude/agent-sdk-prewarm.ts @@ -5,7 +5,7 @@ import { raceWithAbort } from "../../utils/async"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; import { readProcessEnvString, toErrorMessage } from "./agent-sdk-json"; import type { createClaudeQueryOptions } from "./agent-sdk-query-options"; -import { drainClaudeTasks } from "./agent-sdk-tasks"; +import { settleClaudeTasks } from "./agent-sdk-tasks"; const CLAUDE_PREWARM_ENV = "AGENT_DRIVER_CLAUDE_PREWARM"; @@ -14,7 +14,11 @@ interface ClaudePrewarmState { readonly detach: () => void; readonly permissionTasks: Set>; readonly processTasks: Set>; + cleanupTask: Promise | null; + failure: { readonly error: unknown } | null; + permanentFailure: { readonly error: unknown } | null; query: WarmQuery | null; + task: Promise; } export interface ClaudeAgentSdkPrewarmOptions { @@ -44,10 +48,8 @@ export class ClaudeAgentSdkPrewarm { readonly #payload: DriverStartInput; readonly #publicToolCallId: (nativeToolCallId: string) => string; readonly #startup: ClaudeAgentSdkPrewarmOptions["startup"]; - #failure: { readonly error: unknown } | null = null; #state: ClaudePrewarmState | null = null; #stopped = false; - #task: Promise | null = null; constructor(options: ClaudeAgentSdkPrewarmOptions) { this.#createQueryOptions = options.createQueryOptions; @@ -62,29 +64,38 @@ export class ClaudeAgentSdkPrewarm { return; } - const task = this.#run(context, signal); - this.#task = task; - const release = () => { - if (this.#task === task) { - this.#task = null; - } + const startedAtMs = Date.now(); + const abortController = new AbortController(); + const onAbort = () => abortController.abort(signal.reason); + signal.addEventListener("abort", onAbort, { once: true }); + if (signal.aborted) { + onAbort(); + } + const state: ClaudePrewarmState = { + abortController, + cleanupTask: null, + detach: () => signal.removeEventListener("abort", onAbort), + failure: null, + permissionTasks: new Set(), + permanentFailure: null, + processTasks: new Set(), + query: null, + task: Promise.resolve(), }; - void task.then(release, (error) => { - this.#failure ??= { error }; - release(); - }); + this.#state = state; + state.task = Promise.resolve().then(() => this.#run(context, state, startedAtMs)); + void state.task.catch(() => {}); } take(): ClaudePrewarmTake { - if (this.#failure !== null) { - throw this.#failure.error; - } - const state = this.#state; - this.#state = null; - state?.detach(); + if (state !== null && state.failure !== null) { + throw state.failure.error; + } - if (state?.query) { + if (state !== null && state.query !== null) { + this.#state = null; + state.detach(); return { abortController: state.abortController, permissionTasks: state.permissionTasks, @@ -93,6 +104,7 @@ export class ClaudeAgentSdkPrewarm { }; } + // A superseded startup still owns every task it may create until cleanup succeeds. state?.abortController.abort("driver.claude.prewarm.superseded"); return { abortController: new AbortController(), @@ -105,62 +117,30 @@ export class ClaudeAgentSdkPrewarm { async stop(context: AgentDriverContext, reason: string, signal: AbortSignal): Promise { this.#stopped = true; const state = this.#state; - const task = this.#task; - state?.detach(); - state?.abortController.abort(reason); - const warmQuery = state?.query; - if (state !== null) { - state.query = null; + if (state === null) { + signal.throwIfAborted(); + return; } - try { - warmQuery?.close(); - } catch (error) { - context.logger.debug("driver.claude.prewarm.close_failed", { - message: toErrorMessage(error, "prewarm close failed"), - reason, - }); - } + state.detach(); + state.abortController.abort(reason); + this.#close(context, state, reason); - if (task !== null) { - await raceWithAbort(task, signal); - } - if (state !== null && state.processTasks.size > 0) { - const retryingFailedCleanup = this.#failure !== null; - await raceWithAbort(drainClaudeTasks(state.processTasks), signal); - if (retryingFailedCleanup) { - this.#failure = null; - } - } - signal.throwIfAborted(); - if (this.#failure !== null) { - throw this.#failure.error; - } - if (this.#state === state) { - this.#state = null; - } + const settlement = + state.failure !== null + ? this.#cleanup(state) + : state.task.then(() => (this.#state === state ? this.#cleanup(state) : Promise.resolve())); + await raceWithAbort(settlement, signal); } - async #run(context: AgentDriverContext, signal: AbortSignal): Promise { - const startedAtMs = Date.now(); - const abortController = new AbortController(); - const onAbort = () => abortController.abort(signal.reason); - signal.addEventListener("abort", onAbort, { once: true }); - if (signal.aborted) { - onAbort(); - } - const state: ClaudePrewarmState = { - abortController, - detach: () => signal.removeEventListener("abort", onAbort), - permissionTasks: new Set(), - processTasks: new Set(), - query: null, - }; - this.#state = state; - + async #run( + context: AgentDriverContext, + state: ClaudePrewarmState, + startedAtMs: number, + ): Promise { try { const options = await this.#createQueryOptions({ - abortController, + abortController: state.abortController, context, nativeSessionId: this.#getNativeSessionId(), payload: this.#payload, @@ -169,14 +149,14 @@ export class ClaudeAgentSdkPrewarm { publicToolCallId: this.#publicToolCallId, }); - if (this.#stopped || abortController.signal.aborted || this.#state !== state) { + if (this.#stopped || state.abortController.signal.aborted || this.#state !== state) { return; } const warmQuery = await this.#startup({ options }); - if (this.#stopped || abortController.signal.aborted || this.#state !== state) { - warmQuery.close(); + if (this.#stopped || state.abortController.signal.aborted || this.#state !== state) { + this.#closeQuery(context, warmQuery, "driver.claude.prewarm.superseded"); return; } @@ -185,22 +165,69 @@ export class ClaudeAgentSdkPrewarm { prewarmMs: Date.now() - startedAtMs, }); } catch (error) { - if (!abortController.signal.aborted && !this.#stopped) { + if (!state.abortController.signal.aborted && !this.#stopped) { context.logger.debug("driver.claude.prewarm.failed", { message: toErrorMessage(error, "Claude prewarm failed."), }); } } finally { try { - if (state.query === null || abortController.signal.aborted || this.#stopped) { - await drainClaudeTasks(state.processTasks); + if (state.query === null || state.abortController.signal.aborted || this.#stopped) { + this.#close(context, state, "driver.claude.prewarm.finished"); + await this.#cleanup(state); } } finally { state.detach(); - if (this.#state === state && state.query === null && state.processTasks.size === 0) { - this.#state = null; - } } } } + + #cleanup(state: ClaudePrewarmState): Promise { + if (state.cleanupTask !== null) { + return state.cleanupTask; + } + + const task = (async () => { + const result = await settleClaudeTasks(state.processTasks); + if (result.status === "failed") { + state.failure = { error: result.firstFailure }; + state.permanentFailure ??= result.firstPermanentFailure; + throw result.firstFailure; + } + + state.failure = null; + if (state.permanentFailure !== null) { + state.failure = state.permanentFailure; + throw state.permanentFailure.error; + } + if (this.#state === state) { + this.#state = null; + } + })().finally(() => { + if (state.cleanupTask === task) { + state.cleanupTask = null; + } + }); + state.cleanupTask = task; + return task; + } + + #close(context: AgentDriverContext, state: ClaudePrewarmState, reason: string): void { + const query = state.query; + state.query = null; + if (query !== null) { + this.#closeQuery(context, query, reason); + } + } + + #closeQuery(context: AgentDriverContext, query: WarmQuery, reason: string): void { + try { + query.close(); + } catch (error) { + context.logger.debug("driver.claude.prewarm.close_failed", { + message: toErrorMessage(error, "prewarm close failed"), + reason, + }); + } + } } diff --git a/src/runtimes/claude/agent-sdk-tasks.ts b/src/runtimes/claude/agent-sdk-tasks.ts index 35ab0c4..59b193b 100644 --- a/src/runtimes/claude/agent-sdk-tasks.ts +++ b/src/runtimes/claude/agent-sdk-tasks.ts @@ -1,6 +1,14 @@ const taskRetries = new WeakMap, () => Promise>(); const retryableTaskFailures = new WeakSet>(); +type ClaudeTaskDrainResult = + | { readonly status: "completed" } + | { + readonly firstFailure: unknown; + readonly firstPermanentFailure: { readonly error: unknown } | null; + readonly status: "failed"; + }; + export function registerClaudeTaskRetry(task: Promise, retry: () => Promise): void { taskRetries.set(task, retry); } @@ -8,23 +16,32 @@ export function registerClaudeTaskRetry(task: Promise, retry: () => Promis export async function drainClaudeTasks( ...taskSets: ReadonlyArray>> ): Promise { + const result = await settleClaudeTasks(...taskSets); + if (result.status === "failed") { + throw result.firstFailure; + } +} + +export async function settleClaudeTasks( + ...taskSets: ReadonlyArray>> +): Promise { const failedRetryableTasks = new Set>(); let failed = false; let firstFailure: unknown; + let firstPermanentFailure: { readonly error: unknown } | null = null; for (;;) { const tasks = taskSets.flatMap((taskSet) => [...taskSet] .filter((task) => !failedRetryableTasks.has(task)) - .map((task) => ({ owner: taskSet, task })), + .map((task) => ({ owner: taskSet, retry: taskRetries.get(task), task })), ); if (tasks.length === 0) { break; } const results = await Promise.allSettled( - tasks.map(({ task }) => { - const retry = taskRetries.get(task); + tasks.map(({ retry, task }) => { if (retry !== undefined && retryableTaskFailures.has(task)) { return Promise.resolve().then(retry); } @@ -38,11 +55,12 @@ export async function drainClaudeTasks( taskRetries.delete(tracked.task); retryableTaskFailures.delete(tracked.task); } else { - if (taskRetries.has(tracked.task)) { + if (tracked.retry !== undefined) { retryableTaskFailures.add(tracked.task); failedRetryableTasks.add(tracked.task); } else { tracked.owner.delete(tracked.task); + firstPermanentFailure ??= { error: result.reason }; } if (!failed) { failed = true; @@ -53,6 +71,7 @@ export async function drainClaudeTasks( } if (failed) { - throw firstFailure; + return { firstFailure, firstPermanentFailure, status: "failed" }; } + return { status: "completed" }; } diff --git a/src/runtimes/driver-event-admission.ts b/src/runtimes/driver-event-admission.ts index 47e4481..ef40638 100644 --- a/src/runtimes/driver-event-admission.ts +++ b/src/runtimes/driver-event-admission.ts @@ -4,7 +4,8 @@ import type { RunId } from "../protocol/id"; const MAX_PENDING_DRIVER_EVENTS = 1_024; const MAX_PENDING_DRIVER_EVENT_BYTES = 32 * 1_024 * 1_024; -const MAX_RUN_TERMINAL_BATCH_BYTES = 1_024 * 1_024; +export const MAX_RUN_TERMINAL_BATCH_EVENTS = MAX_PENDING_DRIVER_EVENTS; +export const MAX_RUN_TERMINAL_BATCH_BYTES = 1_024 * 1_024; export interface QueuedDriverEvent { readonly bytes: number; @@ -49,14 +50,12 @@ export function preflightDriverEventPush( events: readonly DriverEventInput[], activeRunId: RunId | null, ): void { - for (const event of events) { - admitDriverEventPush( - [event], - activeRunId, - Symbol("driver-event-preflight"), - EMPTY_DRIVER_EVENT_ADMISSION_STATE, - ); - } + admitDriverEventPush( + events, + activeRunId, + Symbol("driver-event-preflight"), + EMPTY_DRIVER_EVENT_ADMISSION_STATE, + ); } export function driverEventBatchBytes(bytes: number, count: number): number { @@ -145,6 +144,12 @@ export function admitDriverEventPush( const losslessCount = terminalBatch ? 0 : losslessEvents.length; + if (terminalBatch && losslessEvents.length > MAX_RUN_TERMINAL_BATCH_EVENTS) { + throw new Error( + `Driver event run terminal batch exceeds ${MAX_RUN_TERMINAL_BATCH_EVENTS} events.`, + ); + } + if ( state.pendingLosslessCount + state.queuedLosslessCount + losslessCount > MAX_PENDING_DRIVER_EVENTS @@ -174,7 +179,6 @@ export function admitDriverEventPush( } const frozenEvents = admittedEvents.map((event) => scopeDriverEvent(event, activeRunId)); - const terminalKey = terminalBatch ? JSON.stringify(frozenEvents) : null; const stampedEvents = withSourceEventIds(frozenEvents); if (new Set(stampedEvents.map((event) => event.sourceEventId)).size !== stampedEvents.length) { @@ -263,6 +267,7 @@ export function admitDriverEventPush( eventCount = serializedLosslessCount; } + const terminalKey = terminalBatch ? JSON.stringify(frozenEvents) : null; const queuedEvents: QueuedDriverEvent[] = []; for (const [index, event] of stampedEvents.entries()) { diff --git a/src/runtimes/driver-event-publisher.ts b/src/runtimes/driver-event-publisher.ts index d93fca2..47e65ac 100644 --- a/src/runtimes/driver-event-publisher.ts +++ b/src/runtimes/driver-event-publisher.ts @@ -142,6 +142,12 @@ export class DriverEventPublisher { } await this.#terminalSettlement?.task; + + const settlement = this.#terminalSettlement; + if (settlement !== null && settlement.nextIndex < settlement.events.length) { + throw new Error("Driver event run terminal settlement slot is full."); + } + const sessionEvents = events.map((event): DriverEventInput => ({ ...event, runId: null })); try { return this.#submit(context, reason, sessionEvents, undefined, true).catch( @@ -192,7 +198,7 @@ export class DriverEventPublisher { if ( settlement !== null && settlement !== terminalSettlement && - (settlement.nextIndex >= settlement.events.length || settlement.task === null) && + settlement.nextIndex >= settlement.events.length && currentRunId !== settlement.activeRunId ) { this.#terminalSettlement = null; @@ -297,22 +303,26 @@ export class DriverEventPublisher { } const activeRunId = context.ports.eventSink.currentRunId(); - const targetRunId = (terminal.runId as RunId | undefined) ?? activeRunId; if (activeRunId === null) { throw new Error("Driver terminal push requires an active run."); } - const scopedEvents = [...closures, terminal].map((event) => - scopeDriverEvent(event, targetRunId), - ); + if (terminal.runId !== undefined && terminal.runId !== activeRunId) { + throw new Error("Driver terminal push must target the active run."); + } + + const targetRunId = activeRunId; + const settlementEvents = [...closures, terminal]; if ( - scopedEvents.some((event) => ((event.runId as RunId | undefined) ?? null) !== targetRunId) + settlementEvents.some((event) => event.runId !== undefined && event.runId !== targetRunId) ) { throw new Error("Driver terminal push requires every event to target the same run."); } + preflightDriverEventPush(settlementEvents, targetRunId); + const scopedEvents = settlementEvents.map((event) => scopeDriverEvent(event, targetRunId)); const key = terminalSettlementKey(scopedEvents); const current = this.#terminalSettlement; @@ -326,19 +336,15 @@ export class DriverEventPublisher { return this.#startTerminalSettlement(context, reason, current); } - const idle = current.nextIndex >= current.events.length || current.task === null; + const settled = current.nextIndex >= current.events.length; const activeRunChanged = activeRunId !== current.activeRunId; - if (!idle || !activeRunChanged) { + if (!settled || !activeRunChanged) { throw new Error("Driver event run terminal settlement slot is full."); } this.#terminalSettlement = null; } - if (targetRunId !== activeRunId) { - throw new Error("Driver terminal push must target the active run."); - } - const retryCandidates = [ ...(this.#inFlightEvents ?? this.#pendingEvents), ...this.#queue.flatMap((entry) => entry.events), @@ -392,8 +398,6 @@ export class DriverEventPublisher { } } - preflightDriverEventPush(events, targetRunId); - if (this.#terminalSettlement !== null) { if (this.#terminalSettlement.key !== key) { throw new Error("Driver event run terminal settlement slot is full."); diff --git a/src/runtimes/mcp/remote-http-mcp-executor.ts b/src/runtimes/mcp/remote-http-mcp-executor.ts index c35067e..c07acac 100644 --- a/src/runtimes/mcp/remote-http-mcp-executor.ts +++ b/src/runtimes/mcp/remote-http-mcp-executor.ts @@ -12,12 +12,9 @@ import { import type { AuthProvider, CallToolResult } from "@modelcontextprotocol/client"; import { AGENT_DRIVER_VERSION } from "../../core/version"; +import type { AgentDriverMcpExecution } from "../../host-ports"; import type { DriverStartInput } from "../../protocol/start"; -import type { - McpExecuteCommand, - McpExternalToolEffectExecution, - McpExternalToolExecutionResult, -} from "../../runtime-command"; +import type { McpExecuteCommand, McpExternalToolExecutionResult } from "../../runtime-command"; import { settlePromiseWithTimeout } from "../../utils/async"; type SessionMcpServer = DriverStartInput["execution"]["session"]["mcpServers"][number]; @@ -221,15 +218,35 @@ function mapMcpExecutionError( return new Error(`Failed to execute MCP tool ${command.toolName} on ${server.name}.`); } -export async function executeRemoteHttpMcpCommand( +async function closeMcpConnection( + client: Client, + transport: StreamableHTTPClientTransport, +): Promise { + await settlePromiseWithTimeout( + Promise.resolve().then(() => transport.terminateSession()), + { + label: "MCP session termination", + timeoutMs: MCP_CLEANUP_TIMEOUT_MS, + }, + ); + await settlePromiseWithTimeout( + Promise.resolve().then(() => client.close()), + { + label: "MCP client close", + timeoutMs: MCP_CLEANUP_TIMEOUT_MS, + }, + ); +} + +export async function prepareRemoteHttpMcpCommand( payload: DriverStartInput, command: McpExecuteCommand, signal: AbortSignal, - effect: McpExternalToolEffectExecution, -): Promise { +): Promise { signal.throwIfAborted(); const server = resolveActiveMcpServer(payload, command); const argumentsObject = parseToolArguments(command); + const proxyUrl = new URL(server.proxyUrl); const authProvider: AuthProvider = { token: async () => server.proxyGrantId, }; @@ -242,45 +259,61 @@ export async function executeRemoteHttpMcpCommand( versionNegotiation: { mode: "auto" }, }, ); - const transport = new StreamableHTTPClientTransport(new URL(server.proxyUrl), { + const transport = new StreamableHTTPClientTransport(proxyUrl, { authProvider, requestInit: { headers: { [MOSOO_TOOL_CALL_ID_HEADER]: command.toolCallId }, }, }); - const requestSignal = AbortSignal.any([signal, AbortSignal.timeout(MCP_REQUEST_TIMEOUT_MS)]); + const connectSignal = AbortSignal.any([signal, AbortSignal.timeout(MCP_REQUEST_TIMEOUT_MS)]); try { await client.connect(transport, { - signal: requestSignal, + signal: connectSignal, timeout: MCP_REQUEST_TIMEOUT_MS, }); - const result = await client.callTool( - { - _meta: { - "io.mosoo/idempotency-key": effect.idempotencyKey, - }, - arguments: argumentsObject, - name: command.toolName, - }, - { - signal: requestSignal, - timeout: MCP_REQUEST_TIMEOUT_MS, - }, - ); - - return normalizeCallToolResult(result, command); } catch (error) { + await closeMcpConnection(client, transport); throw mapMcpExecutionError(command, server, error); - } finally { - await settlePromiseWithTimeout(transport.terminateSession(), { - label: "MCP session termination", - timeoutMs: MCP_CLEANUP_TIMEOUT_MS, - }); - - await settlePromiseWithTimeout(client.close(), { - label: "MCP client close", - timeoutMs: MCP_CLEANUP_TIMEOUT_MS, - }); } + + let disposed = false; + let executed = false; + + return { + async execute(effect): Promise { + if (disposed || executed) { + throw new Error(`Prepared MCP command ${command.commandId} can only be executed once.`); + } + executed = true; + const requestSignal = AbortSignal.any([signal, AbortSignal.timeout(MCP_REQUEST_TIMEOUT_MS)]); + + try { + const result = await client.callTool( + { + _meta: { + "io.mosoo/idempotency-key": effect.idempotencyKey, + }, + arguments: argumentsObject, + name: command.toolName, + }, + { + signal: requestSignal, + timeout: MCP_REQUEST_TIMEOUT_MS, + }, + ); + + return normalizeCallToolResult(result, command); + } catch (error) { + throw mapMcpExecutionError(command, server, error); + } + }, + async [Symbol.asyncDispose](): Promise { + if (disposed) { + return; + } + disposed = true; + await closeMcpConnection(client, transport); + }, + }; } diff --git a/src/runtimes/mcp/server-key.ts b/src/runtimes/mcp/server-key.ts index 1415854..8c2109f 100644 --- a/src/runtimes/mcp/server-key.ts +++ b/src/runtimes/mcp/server-key.ts @@ -1,7 +1,10 @@ import type { DriverBootMcpServer } from "../../protocol/boot"; +const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]); + export function toMcpServerKey(server: DriverBootMcpServer, usedNames: Set): string { - const baseName = server.name.trim() || server.serverId; + const name = server.name.trim(); + const baseName = name.length === 0 || UNSAFE_OBJECT_KEYS.has(name) ? server.serverId : name; let candidate = baseName; let suffix = 2; diff --git a/src/runtimes/openai/app-server-client.ts b/src/runtimes/openai/app-server-client.ts index 820f983..b35281c 100644 --- a/src/runtimes/openai/app-server-client.ts +++ b/src/runtimes/openai/app-server-client.ts @@ -51,8 +51,12 @@ import { jsonRpcResponseSchema } from "./app-server-protocol-client-schemas"; import { toOpenAiProtocolError } from "./app-server-event-mapping"; interface PendingJsonRpcRequest { + accept: ((value: unknown) => Promise) | null; method: string; + parse(value: unknown): unknown; reject(error: Error): void; + rejectAtBarrier: ((error: Error) => Promise) | null; + responseQueued: boolean; resolve(value: unknown): void; } @@ -201,6 +205,8 @@ export class OpenAiAppServerClient { #processTarget: BoundSpawnedProcess | null = null; #processClosed: Promise | null = null; #processTreeMarker: string | null = null; + #protocolFailureCommit: Promise | null = null; + #protocolFailureTask: Promise | null = null; #readline: ReadlineInterface | null = null; #serverMessageQueue: Promise = Promise.resolve(); #serverMessagesPaused = false; @@ -213,7 +219,9 @@ export class OpenAiAppServerClient { this.#context = context; this.#requestHandler = new OpenAiAppServerRequestHandler({ context, - handleError: async (error) => this.#failProtocol(error), + handleError: async (error) => { + void this.#failProtocol(error); + }, isStopped: () => this.#stopRequested, mapToolCallId: context.mapToolCallId, respond: (id, result) => this.respond(id, result), @@ -229,24 +237,21 @@ export class OpenAiAppServerClient { this.#startRequested = true; const mcpConfig = buildOpenAiMcpServerConfig(this.#payload.execution.session.mcpServers); - const processTree = createProcessTreeEnvironment( - buildRuntimeChildProcessEnv(this.#payload.execution.environment.paths, { - ...process.env, - ...this.#payload.execution.environment.variables, - ...mcpConfig.env, - [OPENAI_RUNTIME_HOME_ENV_NAME]: this.#payload.execution.session.homePath, - LOG_FORMAT: "json", - }), - ); - const env = processTree.env; - this.#processTreeMarker = processTree.marker; + const env = buildRuntimeChildProcessEnv(this.#payload.execution.environment.paths, { + ...process.env, + ...this.#payload.execution.environment.variables, + ...mcpConfig.env, + [OPENAI_RUNTIME_HOME_ENV_NAME]: this.#payload.execution.session.homePath, + LOG_FORMAT: "json", + }); const onAbort = () => { this.#stopRequested = true; const child = this.#process; const target = this.#processTarget; + const marker = this.#processTreeMarker; - if (child !== null && target !== null) { - signalAppServerSession(target, processTree.marker, "SIGKILL"); + if (child !== null && target !== null && marker !== null) { + signalAppServerSession(target, marker, "SIGKILL"); } }; signal?.addEventListener("abort", onAbort, { once: true }); @@ -305,12 +310,20 @@ export class OpenAiAppServerClient { await measure("app_server.spawn", async () => { const executable = readRuntimeExecutable(); - const child = spawn(executable, ["app-server"], { - cwd: this.#payload.execution.session.cwd, - detached: true, - env, - stdio: ["pipe", "pipe", "pipe"], - }); + const processTree = createProcessTreeEnvironment(env); + this.#processTreeMarker = processTree.marker; + let child: ChildProcessWithoutNullStreams; + try { + child = spawn(executable, ["app-server"], { + cwd: this.#payload.execution.session.cwd, + detached: true, + env: processTree.env, + stdio: ["pipe", "pipe", "pipe"], + }); + } catch (error) { + this.#releaseProcessTreeMarker(processTree.marker); + throw error; + } const target = bindSpawnedProcess(child, process.platform, processTree); this.#process = child; @@ -348,10 +361,10 @@ export class OpenAiAppServerClient { this.#onLine(line); }); limitedStdout.once("error", (error) => { - this.#failProtocol(error); + void this.#failProtocol(error); }); child.stdin.on("error", (error) => { - this.#failProtocol(error); + void this.#failProtocol(error); }); child.stderr.setEncoding("utf8"); child.stderr.on("data", (chunk: string) => { @@ -388,7 +401,7 @@ export class OpenAiAppServerClient { ? cleanupError : new Error("OpenAi app-server process-tree cleanup failed."); } - await this.#failAfterDrain(failure); + await this.#failProtocol(failure); })(); }); @@ -407,7 +420,7 @@ export class OpenAiAppServerClient { await waitForLinuxProcessMarkerExit(processTree.marker); })(), ); - this.#failProtocol(error); + void this.#failProtocol(error); throw error; } @@ -417,7 +430,7 @@ export class OpenAiAppServerClient { return; } signalLinuxProcessMarker(processTree.marker, "SIGKILL"); - this.#failProtocol(error); + void this.#failProtocol(error); }; void watchdog.cleanup.then( () => @@ -448,7 +461,7 @@ export class OpenAiAppServerClient { throw error; } child.on("error", (error) => { - this.#failProtocol(error); + void this.#failProtocol(error); }); }); @@ -488,30 +501,41 @@ export class OpenAiAppServerClient { params: ClientRequestParams[M], signal?: AbortSignal, ): Promise { - return this.#request(method, params, CLIENT_REQUEST_RESULT_PARSERS[method], signal); + return this.#request(method, params, CLIENT_REQUEST_RESULT_PARSERS[method], null, signal); } - async cleanBackgroundTerminals(threadId: string, signal?: AbortSignal): Promise { - await this.request("thread/backgroundTerminals/clean", { threadId }, signal); + async requestAtWireBarrier( + method: M, + params: ClientRequestParams[M], + handlers: { + accept(result: ClientRequestResult[M]): Promise; + reject(error: Error): Promise; + }, + signal?: AbortSignal, + ): Promise { + return this.#request(method, params, CLIENT_REQUEST_RESULT_PARSERS[method], handlers, signal); } - async #request( + async #request( method: string, params: unknown, parseResult: (value: unknown) => T, + handlers: { accept(result: T): Promise; reject(error: Error): Promise } | null, signal?: AbortSignal, - ): Promise { + ): Promise { signal?.throwIfAborted(); const id = this.#nextId; this.#nextId += 1; - const response = Promise.withResolvers(); + const response = Promise.withResolvers(); this.#pendingRequests.set(id, { + accept: handlers === null ? null : async (value: unknown) => handlers.accept(value as T), method, + parse: parseResult, reject: response.reject, - resolve: (value) => { - response.resolve(parseResult(value)); - }, + rejectAtBarrier: handlers?.reject ?? null, + responseQueued: false, + resolve: response.resolve, }); try { @@ -537,7 +561,7 @@ export class OpenAiAppServerClient { } if (result.status === "timed_out") { - this.#failProtocol(result.error); + await this.#failProtocol(result.error); } throw result.error; @@ -565,6 +589,9 @@ export class OpenAiAppServerClient { }); if (tail === this.#serverMessageQueue) { + if (this.#fatalError !== null) { + throw this.#fatalError; + } return; } } @@ -684,10 +711,11 @@ export class OpenAiAppServerClient { } #releaseProcessTreeMarker(marker: string): void { - releaseLinuxProcessMarker(marker); - if (this.#processTreeMarker === marker) { - this.#processTreeMarker = null; + if (this.#processTreeMarker !== marker) { + return; } + this.#processTreeMarker = null; + releaseLinuxProcessMarker(marker); } #send(message: Record): void { @@ -705,7 +733,7 @@ export class OpenAiAppServerClient { } #onLine(line: string): void { - if (this.#stopRequested) { + if (this.#stopRequested || this.#fatalError !== null) { return; } @@ -720,12 +748,14 @@ export class OpenAiAppServerClient { try { parsed = JSON.parse(trimmed); } catch { - this.#failProtocol(new TypeError("OpenAi app-server stdout is not valid JSON.")); + void this.#failProtocol(new TypeError("OpenAi app-server stdout is not valid JSON.")); return; } if (!isRecord(parsed)) { - this.#failProtocol(new TypeError("OpenAi app-server protocol message must be an object.")); + void this.#failProtocol( + new TypeError("OpenAi app-server protocol message must be an object."), + ); return; } @@ -734,7 +764,7 @@ export class OpenAiAppServerClient { if ("result" in parsed || "error" in parsed) { if (id === null) { - this.#failProtocol(new TypeError("OpenAi app-server response requires a valid id.")); + void this.#failProtocol(new TypeError("OpenAi app-server response requires a valid id.")); return; } @@ -745,8 +775,11 @@ export class OpenAiAppServerClient { if (method !== null) { const bytes = Buffer.byteLength(trimmed, "utf8"); - if (bytes > MAX_PENDING_SERVER_MESSAGE_BYTES - this.#pendingServerMessageBytes) { - this.#failProtocol(new Error("App-server message queue limit exceeded.")); + if ( + this.#pendingServerMessages >= MAX_PENDING_SERVER_MESSAGES || + bytes > MAX_PENDING_SERVER_MESSAGE_BYTES - this.#pendingServerMessageBytes + ) { + void this.#failProtocol(new Error("App-server message queue limit exceeded.")); return; } @@ -771,7 +804,7 @@ export class OpenAiAppServerClient { return; } - this.#failProtocol( + void this.#failProtocol( new TypeError("OpenAi app-server protocol message requires a valid method or id."), ); } @@ -797,6 +830,7 @@ export class OpenAiAppServerClient { if ( !this.#serverMessagesPaused || this.#stopRequested || + this.#fatalError !== null || this.#pendingServerMessages > RESUME_PENDING_SERVER_MESSAGES || this.#pendingServerMessageBytes > RESUME_PENDING_SERVER_MESSAGE_BYTES ) { @@ -811,23 +845,58 @@ export class OpenAiAppServerClient { this.#process?.stdout.resume(); } - #failProtocol(error: Error): void { + #failProtocol(error: Error): Promise { if (this.#stopRequested) { - return; + return Promise.resolve(); + } + + if (this.#protocolFailureTask !== null) { + return this.#protocolFailureTask; + } + if (this.#fatalError !== null) { + return this.#protocolFailureCommit ?? Promise.resolve(); } this.#fatalError = error; - this.#rejectPending(error); - void this.#notifyProtocolError(error); - void this.stop().catch(() => {}); + this.#process?.stdout.pause(); + const barrier = this.#serverMessageQueue; + const task = (async () => { + await barrier; + await this.#commitProtocolFailure(error); + })(); + this.#protocolFailureTask = task; + this.#serverMessageQueue = task; + return task; } - async #failAfterDrain(error: Error): Promise { - await this.drainServerMessages(); + async #failQueuedProtocol(error: Error): Promise { + if (this.#stopRequested) { + return; + } - if (!this.#stopRequested) { - this.#failProtocol(error); + // This message was admitted before any out-of-band failure waiting on the + // queue barrier, so its failure is authoritative in wire order. + if (this.#protocolFailureCommit === null) { + this.#fatalError = error; } + this.#process?.stdout.pause(); + await this.#commitProtocolFailure(this.#fatalError ?? error); + } + + #commitProtocolFailure(error: Error): Promise { + if (this.#protocolFailureCommit !== null) { + return this.#protocolFailureCommit; + } + + const task = this.#finishProtocolFailure(error); + this.#protocolFailureCommit = task; + return task; + } + + async #finishProtocolFailure(error: Error): Promise { + await this.#notifyProtocolError(error); + this.#rejectPending(error); + void this.stop().catch(() => {}); } async #notifyProtocolError(error: Error): Promise { @@ -864,7 +933,7 @@ export class OpenAiAppServerClient { }); if (id === null) { - this.#failProtocol(failure); + await this.#failQueuedProtocol(failure); return; } @@ -888,7 +957,7 @@ export class OpenAiAppServerClient { const parsed = jsonRpcResponseSchema.safeParse(message); if (!parsed.success) { - this.#failProtocol( + void this.#failProtocol( new TypeError("OpenAi app-server response envelope is invalid.", { cause: parsed.error, }), @@ -898,40 +967,137 @@ export class OpenAiAppServerClient { const pending = this.#pendingRequests.get(id); - if (pending === undefined) { + if (pending === undefined || pending.responseQueued) { return; } + if (pending.accept === null) { + this.#settleResponse(id, pending, parsed.data); + return; + } + pending.responseQueued = true; + this.#serverMessageQueue = this.#processResponse( + this.#serverMessageQueue, + id, + pending, + parsed.data, + ); + } - this.#pendingRequests.delete(id); + async #processResponse( + previousMessage: Promise, + id: RequestId, + pending: PendingJsonRpcRequest, + response: + | { error: { code: number; data?: unknown; message: string }; id: RequestId } + | { + id: RequestId; + result: unknown; + }, + ): Promise { + await previousMessage; - if ("error" in parsed.data) { - const { error } = parsed.data; + if (this.#pendingRequests.get(id) !== pending) { + return; + } + const accept = pending.accept; + const rejectAtBarrier = pending.rejectAtBarrier; + if (accept === null || rejectAtBarrier === null) { + await this.#failQueuedProtocol(new Error("Wire-barrier response handlers are missing.")); + return; + } - this.#context.logger.error( - "driver.openai.client_request.failed", - new Error(toOpenAiProtocolError({ message: error.message }).message), - { - data: summarizeJsonRpcErrorData(error.data), - method: pending.method, - responseCode: error.code, - }, - ); - pending.reject(new Error(error.message)); + if ("error" in response) { + const error = this.#reportResponseError(pending, response.error); + this.#pendingRequests.delete(id); + try { + await rejectAtBarrier(error); + pending.reject(error); + } catch (rejectionError) { + const failure = + rejectionError instanceof Error + ? rejectionError + : new Error("OpenAi app-server response rejection failed."); + await this.#failQueuedProtocol(failure); + pending.reject(failure); + } return; } + let result: unknown; try { - pending.resolve(parsed.data.result); + result = pending.parse(response.result); } catch (parseError) { const error = parseError instanceof Error ? parseError : new Error("OpenAi app-server result parse failed."); + await this.#failQueuedProtocol(error); + return; + } + + this.#pendingRequests.delete(id); + try { + pending.resolve(await accept(result)); + } catch (acceptError) { + const error = + acceptError instanceof Error + ? acceptError + : new Error("OpenAi app-server response acceptance failed."); + await this.#failQueuedProtocol(error); pending.reject(error); - this.#failProtocol(error); } } + #settleResponse( + id: RequestId, + pending: PendingJsonRpcRequest, + response: + | { error: { code: number; data?: unknown; message: string }; id: RequestId } + | { id: RequestId; result: unknown }, + ): void { + if ("error" in response) { + this.#rejectResponse(id, pending, response.error); + return; + } + + try { + pending.resolve(pending.parse(response.result)); + this.#pendingRequests.delete(id); + } catch (parseError) { + const error = + parseError instanceof Error + ? parseError + : new Error("OpenAi app-server result parse failed."); + void this.#failProtocol(error); + } + } + + #rejectResponse( + id: RequestId, + pending: PendingJsonRpcRequest, + error: { code: number; data?: unknown; message: string }, + ): void { + this.#pendingRequests.delete(id); + pending.reject(this.#reportResponseError(pending, error)); + } + + #reportResponseError( + pending: PendingJsonRpcRequest, + error: { code: number; data?: unknown; message: string }, + ): Error { + const failure = new Error(error.message); + this.#context.logger.error( + "driver.openai.client_request.failed", + new Error(toOpenAiProtocolError({ message: error.message }).message), + { + data: summarizeJsonRpcErrorData(error.data), + method: pending.method, + responseCode: error.code, + }, + ); + return failure; + } + async #onServerMessage(message: JsonObject, method: string, id: RequestId | null): Promise { if (id === null) { if (isServerRequestMethod(method)) { @@ -972,7 +1138,7 @@ export class OpenAiAppServerClient { const request = parseServerRequest(message)!; if (this.#requestHandler.isPending(request.id)) { - this.#failProtocol( + await this.#failQueuedProtocol( new Error(`OpenAi app-server request ${String(request.id)} is already pending.`), ); return; diff --git a/src/runtimes/openai/app-server-driver-backend.ts b/src/runtimes/openai/app-server-driver-backend.ts index 62f9907..f148671 100644 --- a/src/runtimes/openai/app-server-driver-backend.ts +++ b/src/runtimes/openai/app-server-driver-backend.ts @@ -34,7 +34,6 @@ import type { ThreadStartResponse, TurnStatus, TurnStartParams, - TurnStartResponse, } from "./app-server-protocol"; /** @@ -229,10 +228,10 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { })(); const skillBootstrapPromise = (async () => { const materializedSkills = await measureStartupPhase("skills.materialize", () => - context.ports.skill.materialize(this.#payload.execution), + context.ports.skill.materialize(this.#payload.execution, signal), ); const artifacts = await measureStartupPhase("skills.bootstrap", () => - writeSkillBootstrapArtifacts(this.#payload.execution), + writeSkillBootstrapArtifacts(this.#payload.execution, materializedSkills, signal), ); return { artifacts, count: materializedSkills.length }; })(); @@ -501,15 +500,12 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { this.#pendingTurnStartServerRequests = null; this.#pendingTurnStartUpdates = null; - let client: OpenAiAppServerClient | null = null; - let threadId: string; - let turnResult: TurnStartResponse; - let turnStartRequestedAtMs: number; + let completion: Promise; try { await this.#ensureClient(context); - client = this.#requireClient(); - threadId = this.#requireThreadId(); + const client = this.#requireClient(); + const threadId = this.#requireThreadId(); context.logger.info("driver.openai.prompt.sending", { textLength: input.text.length, @@ -520,8 +516,8 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { threadIdPresent: true, }); - turnStartRequestedAtMs = Date.now(); - turnResult = await client.request( + const turnStartRequestedAtMs = Date.now(); + const accepted = await client.requestAtWireBarrier( "turn/start", createTurnParams({ approvalPolicy: resolveApprovalPolicy(this.#payload), @@ -530,7 +526,56 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { text: input.text, threadId, }), + { + accept: async (turnResult) => { + const turnId = turnResult.turn.id; + const publicTurnId = this.#events.publicTurnId(turnId); + const turnCompletion = this.#events.trackTurn(turnId, runId); + void turnCompletion.catch(() => {}); + this.#claimTurnStartResponse(); + + const turnStartedAtMs = Date.now(); + await this.#eventPublisher.push(context, "driver.openai.provider.turn_start", [ + createTimingEvent({ + completedAt: new Date(turnStartedAtMs).toISOString(), + path: "unknown", + phases: [ + createTimingPhase( + "provider.turn_start", + toDurationMs(turnStartRequestedAtMs, turnStartedAtMs), + ), + ], + runId, + sessionId: context.payload.execution.run.sessionId, + sourceEventId: `openai.provider.turn_start:${publicTurnId}`, + stage: "driver_turn", + startedAt: new Date(turnStartRequestedAtMs).toISOString(), + native: { + eventName: "provider.turn_start", + provider: "openai", + turnId: publicTurnId, + }, + }), + ]); + + await this.#events.publishRunStarted(context, { runId, turnId }); + + if (isTerminalTurn(turnResult.turn.status)) { + await this.#events.handleNotification(context, "turn/completed", { + threadId, + turn: turnResult.turn, + }); + } + + return { completion: turnCompletion }; + }, + reject: async (error) => { + this.#claimTurnStartResponse(); + await this.#publishTurnStartFailure(context, runId, error); + }, + }, ); + completion = accepted.completion; } catch (error) { const publishCancellation = this.#pendingTurnStartCancellationEvent; const pendingCancellationReason = this.#pendingTurnStartCancellationReason; @@ -560,68 +605,6 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { throw new Error(toOpenAiProtocolError({ message: failure.message }).message); } - const turnId = turnResult.turn.id; - const publicTurnId = this.#events.publicTurnId(turnId); - const completion = this.#events.trackTurn(turnId, runId); - void completion.catch(() => {}); - const publishCancellation = this.#pendingTurnStartCancellationEvent; - const pendingCancellationReason = this.#pendingTurnStartCancellationReason; - - if (pendingCancellationReason !== null) { - await this.#finishPendingTurnStartCancellation( - context, - runId, - pendingCancellationReason, - publishCancellation, - ); - this.#events.rejectTurn(turnId, new DriverTurnCancelledError(pendingCancellationReason)); - this.#events.releaseTurnState(); - this.#turnStartInFlight = false; - this.#turnStartRunId = null; - this.#pendingTurnStartCancellationEvent = false; - this.#pendingTurnStartCancellationReason = null; - throw new DriverTurnCancelledError(pendingCancellationReason); - } - - this.#turnStartInFlight = false; - this.#turnStartRunId = null; - this.#pendingTurnStartCancellationEvent = false; - this.#pendingTurnStartCancellationReason = null; - - const turnStartedAtMs = Date.now(); - - await this.#eventPublisher.push(context, "driver.openai.provider.turn_start", [ - createTimingEvent({ - completedAt: new Date(turnStartedAtMs).toISOString(), - path: "unknown", - phases: [ - createTimingPhase( - "provider.turn_start", - toDurationMs(turnStartRequestedAtMs, turnStartedAtMs), - ), - ], - runId, - sessionId: context.payload.execution.run.sessionId, - sourceEventId: `openai.provider.turn_start:${publicTurnId}`, - stage: "driver_turn", - startedAt: new Date(turnStartRequestedAtMs).toISOString(), - native: { - eventName: "provider.turn_start", - provider: "openai", - turnId: publicTurnId, - }, - }), - ]); - - await this.#events.publishRunStarted(context, { runId, turnId }); - - if (isTerminalTurn(turnResult.turn.status)) { - await this.#events.handleNotification(context, "turn/completed", { - threadId, - turn: turnResult.turn, - }); - } - await completion; } @@ -631,6 +614,26 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { return false; } this.#turnStartRunId = null; + await this.#publishTurnStartFailure(context, runId, error); + return true; + } + + #claimTurnStartResponse(): void { + this.#turnStartInFlight = false; + this.#turnStartRunId = null; + this.#pendingTurnStartCancellationEvent = false; + this.#pendingTurnStartCancellationReason = null; + } + + async #publishTurnStartFailure( + context: AgentDriverContext, + runId: RunId, + error: Error, + ): Promise { + const protocolError = { + ...toOpenAiProtocolError({ message: error.message }), + code: "openai.provider_failed", + } as const; await this.#eventPublisher.pushTerminal( context, @@ -644,13 +647,20 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { runId, sourceEventId: `openai.provider.failed.started:${runId}`, }, + ...this.#events + .turnStartTerminalEvents({ error: protocolError, kind: "failed" }) + .map((event, index) => ({ + ...event, + runId, + sourceEventId: + event.sourceEventId ?? `openai.provider.failed.closure:${runId}:${String(index)}`, + })), ], { kind: "run.failed", payload: { error: { - ...toOpenAiProtocolError({ message: error.message }), - code: "openai.provider_failed", + ...protocolError, }, recoverable: false, }, @@ -658,7 +668,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { sourceEventId: `openai.provider.failed.terminal:${runId}`, }, ); - return true; + this.#events.releaseTurnState(); } async #publishTurnStartCancellation( @@ -688,7 +698,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { runId, sourceEventId: `openai.turn_start.cancelled.requested:${runId}`, }, - ...this.#events.turnStartCancellationEvents().map((event, index) => ({ + ...this.#events.turnStartTerminalEvents({ kind: "cancelled" }).map((event, index) => ({ ...event, runId, sourceEventId: @@ -811,8 +821,9 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { if (activeTurnIds.length === 0) { try { - await client.cleanBackgroundTerminals( - threadId, + await client.request( + "thread/backgroundTerminals/clean", + { threadId }, AbortSignal.timeout(OPENAI_BACKGROUND_TERMINAL_CLEAN_TIMEOUT_MS), ); await serverRequests; diff --git a/src/runtimes/openai/app-server-event-bridge.ts b/src/runtimes/openai/app-server-event-bridge.ts index 11e27ff..62d28d3 100644 --- a/src/runtimes/openai/app-server-event-bridge.ts +++ b/src/runtimes/openai/app-server-event-bridge.ts @@ -10,6 +10,7 @@ import { OpenAiPublicIdState, OpenAiSessionUsageState, OpenAiToolState, + type OpenAiTerminalOutcome, } from "./app-server-event-state"; import { chunkOpenAiText, OpenAiAppServerItemEventBridge } from "./app-server-item-events"; import { isRecord, readArray, readNonEmptyString, readRecord, readString } from "./app-server-json"; @@ -309,8 +310,8 @@ export class OpenAiAppServerEventBridge { this.#itemEvents.reset(); } - turnStartCancellationEvents(): DriverEventInput[] { - return this.#itemEvents.terminalEvents({ kind: "cancelled" }); + turnStartTerminalEvents(outcome: OpenAiTerminalOutcome): DriverEventInput[] { + return this.#itemEvents.terminalEvents(outcome); } async trackTurn(turnId: string, runId: RunId): Promise { diff --git a/src/runtimes/openai/app-server-event-mapping.ts b/src/runtimes/openai/app-server-event-mapping.ts index 992f7aa..224981f 100644 --- a/src/runtimes/openai/app-server-event-mapping.ts +++ b/src/runtimes/openai/app-server-event-mapping.ts @@ -33,7 +33,7 @@ function readNonNegativeNumber(value: JsonObject | null, key: string): number | return typeof entry === "number" && Number.isSafeInteger(entry) && entry >= 0 ? entry : null; } -export function toOpenAiErrorMessage( +function toOpenAiErrorMessage( message: string, additionalDetails: string | null | undefined, ): string { diff --git a/src/runtimes/openai/app-server-protocol-types.ts b/src/runtimes/openai/app-server-protocol-types.ts index d4c99d7..d92e77f 100644 --- a/src/runtimes/openai/app-server-protocol-types.ts +++ b/src/runtimes/openai/app-server-protocol-types.ts @@ -1,6 +1,5 @@ import type { ClientRequest as GeneratedClientRequest, - InitializeParams as GeneratedInitializeParams, RequestId as GeneratedRequestId, ServerNotification as GeneratedServerNotification, ServerRequest as GeneratedServerRequest, @@ -8,13 +7,11 @@ import type { import type { AskForApproval, CurrentTimeReadResponse as GeneratedCurrentTimeReadResponse, - SandboxPolicy as GeneratedSandboxPolicy, ThreadInjectItemsParams as GeneratedThreadInjectItemsParams, ThreadResumeParams as GeneratedThreadResumeParams, ThreadStartParams as GeneratedThreadStartParams, TurnStartParams as GeneratedTurnStartParams, TurnStatus as GeneratedTurnStatus, - UserInput as GeneratedUserInput, } from "./generated/v2"; import type { JsonObject } from "./app-server-json"; @@ -25,10 +22,6 @@ type WireServerNotification = Exclude< export type RequestId = GeneratedRequestId; export type ApprovalPolicy = AskForApproval; -export type SandboxPolicy = GeneratedSandboxPolicy; -export type UserInput = GeneratedUserInput; - -export type InitializeParams = GeneratedInitializeParams; export type ThreadStartParams = GeneratedThreadStartParams; export type ThreadResumeParams = GeneratedThreadResumeParams; export type ThreadInjectItemsParams = GeneratedThreadInjectItemsParams; diff --git a/src/runtimes/openai/auth-state.ts b/src/runtimes/openai/auth-state.ts index 17c34de..13c73ae 100644 --- a/src/runtimes/openai/auth-state.ts +++ b/src/runtimes/openai/auth-state.ts @@ -1,4 +1,5 @@ -import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises"; +import { randomUUID } from "node:crypto"; +import { mkdir, open, readFile, rename, unlink } from "node:fs/promises"; import { join } from "node:path"; import type { JsonObject, JsonValue } from "../../protocol/json"; @@ -117,33 +118,41 @@ function stringifyToml(object: Record): string { return `${lines.join("\n")}\n`; } -async function writeFileIfChanged( - path: string, - contents: string, - options?: { mode?: number }, -): Promise { +async function writeFileIfChanged(path: string, contents: string): Promise { const existing = await readFile(path, "utf8").catch(() => null); if (existing === contents) { - if (options?.mode !== undefined) { - const fileStat = await stat(path); - const currentMode = fileStat.mode & 0o777; - - if (currentMode !== options.mode) { - await chmod(path, options.mode); - } - } - return false; } - await writeFile(path, contents, { encoding: "utf8" }); + await writeFileAtomically(path, contents); + return true; +} - if (options?.mode !== undefined) { - await chmod(path, options.mode); +async function writeFileAtomically(path: string, contents: string, mode = 0o666): Promise { + const temporaryPath = `${path}.${randomUUID()}.tmp`; + let temporaryCreated = false; + + try { + const temporary = await open(temporaryPath, "wx", mode); + temporaryCreated = true; + try { + await temporary.writeFile(contents, "utf8"); + await temporary.sync(); + } finally { + await temporary.close(); + } + await rename(temporaryPath, path); + temporaryCreated = false; + } finally { + if (temporaryCreated) { + await unlink(temporaryPath).catch((error: unknown) => { + if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) { + throw error; + } + }); + } } - - return true; } export async function materializeOpenAiApiKeyAuthState( @@ -161,7 +170,7 @@ export async function materializeOpenAiApiKeyAuthState( } await mkdir(input.runtimeHome, { recursive: true }); - const written = await writeFileIfChanged( + await writeFileAtomically( authJsonPath, `${JSON.stringify( { @@ -173,13 +182,13 @@ export async function materializeOpenAiApiKeyAuthState( null, 2, )}\n`, - { mode: 0o600 }, + 0o600, ); return { authJsonPath, hasApiKey: true, - written, + written: true, }; } diff --git a/src/runtimes/provider-registry.ts b/src/runtimes/provider-registry.ts index 5a70d11..f4896b2 100644 --- a/src/runtimes/provider-registry.ts +++ b/src/runtimes/provider-registry.ts @@ -1,4 +1,3 @@ -import type { AgentDriverHostPortName } from "../host-ports"; import type { DriverRuntime, DriverRuntimeTransport } from "../protocol/runtime"; import type { DriverStartInput } from "../protocol/start"; import type { DriverCapability } from "../runtime-command"; @@ -11,23 +10,9 @@ export interface AgentDriverProviderDescriptor { readonly capabilities: readonly DriverCapability[]; createBackend(input: DriverStartInput): AgentDriverBackend; readonly id: DriverRuntimeTransport; - readonly requiredHostPorts: readonly AgentDriverHostPortName[]; readonly runtime: DriverRuntime; } -export interface AgentDriverProviderRegistry { - createBackend(input: DriverStartInput): AgentDriverBackend; - getByStartInput(input: DriverStartInput): AgentDriverProviderDescriptor; - list(): readonly AgentDriverProviderDescriptor[]; -} - -const SHARED_REQUIRED_HOST_PORTS = [ - "event_sink", - "permission", - "mcp", - "skill", -] as const satisfies readonly AgentDriverHostPortName[]; - const TEXT_TOOL_CAPABILITIES = [ { id: "custom_tool_execute", status: "unsupported", version: 1 }, { id: "file_change", status: "supported", version: 1 }, @@ -51,7 +36,6 @@ const PROVIDERS = [ ], createBackend: (payload) => new OpenAiAppServerDriverBackend(payload), id: "openai-app-server", - requiredHostPorts: SHARED_REQUIRED_HOST_PORTS, runtime: "openai-runtime", }, { @@ -62,7 +46,6 @@ const PROVIDERS = [ ], createBackend: (payload) => new ClaudeAgentSdkDriverBackend(payload), id: "claude-agent-sdk", - requiredHostPorts: SHARED_REQUIRED_HOST_PORTS, runtime: "claude-agent-sdk", }, { @@ -73,34 +56,17 @@ const PROVIDERS = [ ], createBackend: (payload) => new AcpDriverBackend(payload), id: "acp-fallback", - requiredHostPorts: [...SHARED_REQUIRED_HOST_PORTS, "file", "host_integration"], runtime: "acp-fallback", }, ] as const satisfies readonly AgentDriverProviderDescriptor[]; -export function createAgentDriverProviderRegistry( - providers: readonly AgentDriverProviderDescriptor[] = PROVIDERS, -): AgentDriverProviderRegistry { - const providersByTransport = new Map(); - - for (const provider of providers) { - registerProviderTransport(providersByTransport, provider, provider.id); - } - - return { - createBackend(input) { - return this.getByStartInput(input).createBackend(input); - }, - getByStartInput(input) { - return resolveProviderForStartInput(providersByTransport, input); - }, - list() { - return providers; - }, - }; -} - -export const AGENT_DRIVER_PROVIDER_REGISTRY = createAgentDriverProviderRegistry(); +export const AGENT_DRIVER_PROVIDER_REGISTRY = { + createBackend(input: DriverStartInput): AgentDriverBackend { + return resolveProviderForStartInput(input).createBackend(input); + }, + getByStartInput: resolveProviderForStartInput, + list: () => PROVIDERS, +}; export function createAgentDriverProviderCapabilities(input: { permissionRequestStatus: DriverCapability["status"]; @@ -121,11 +87,8 @@ export function createAgentDriverProviderCapabilities(input: { return [...capabilitiesById.values()]; } -function resolveProviderForStartInput( - providersByTransport: Map, - input: DriverStartInput, -): AgentDriverProviderDescriptor { - const provider = providersByTransport.get(input.runtimeTransport); +function resolveProviderForStartInput(input: DriverStartInput): AgentDriverProviderDescriptor { + const provider = PROVIDERS.find((candidate) => candidate.id === input.runtimeTransport); if (!provider) { throw new Error(`Unsupported runtime transport: ${input.runtimeTransport}.`); @@ -144,19 +107,3 @@ function resolveProviderForStartInput( return provider; } - -function registerProviderTransport( - providersByTransport: Map, - provider: AgentDriverProviderDescriptor, - transport: DriverRuntimeTransport, -): void { - const existing = providersByTransport.get(transport); - - if (existing) { - throw new Error( - `Runtime transport ${transport} is already registered by provider ${existing.id}.`, - ); - } - - providersByTransport.set(transport, provider); -} diff --git a/src/runtimes/runtime-turn-transcript.ts b/src/runtimes/runtime-turn-transcript.ts index 555076d..f6afea3 100644 --- a/src/runtimes/runtime-turn-transcript.ts +++ b/src/runtimes/runtime-turn-transcript.ts @@ -4,10 +4,6 @@ import type { MessageId } from "../protocol/id"; export class RuntimeAssistantMessageIdIndex { readonly #messageIds = new Map(); - get(key: TKey): MessageId | null { - return this.#messageIds.get(key) ?? null; - } - getOrCreate(key: TKey): MessageId { const existing = this.#messageIds.get(key); diff --git a/src/runtimes/skill-bootstrap.ts b/src/runtimes/skill-bootstrap.ts index 7f15455..2aab8a8 100644 --- a/src/runtimes/skill-bootstrap.ts +++ b/src/runtimes/skill-bootstrap.ts @@ -1,9 +1,27 @@ import { createHash } from "node:crypto"; -import { mkdir, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { readlink, symlink, unlink } from "node:fs/promises"; +import type { FileHandle } from "node:fs/promises"; +import { dirname, join, relative, resolve } from "node:path"; +import type { AgentDriverMaterializedSkill } from "../host-ports"; +import type { Logger } from "../observability"; import type { DriverSkillCatalogEntry } from "../protocol/boot"; import type { DriverExecutionInput } from "../protocol/execution"; +import { + assertDirectoryIdentity, + closeFileHandles, + cleanupAtomicWriteTemporaryFiles, + directoryEntryPath, + ensureAbsoluteRealDirectory, + ensureRealDirectoryAt, + openAbsoluteRealDirectory, + openRealDirectory, + readDirectoryEntriesBounded, + writeFileAtomically, +} from "./atomic-file"; + +const NATIVE_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +const MAX_NATIVE_SKILL_ALIAS_ENTRIES = 1_024; interface SkillCatalogManifestEntry { frontmatter: DriverSkillCatalogEntry["frontmatter"]; @@ -23,21 +41,394 @@ function getSkillCatalogRoot(execution: DriverExecutionInput): string { return join(execution.session.sharedRootPath, ".mosoo", "skills"); } +function hasErrorCode(error: unknown, code: string): boolean { + return typeof error === "object" && error !== null && "code" in error && error.code === code; +} + +async function openOptionalRealDirectory(path: string, label: string): Promise { + try { + return await openRealDirectory(path, label); + } catch (error) { + if (hasErrorCode(error, "ENOENT")) { + return null; + } + throw error; + } +} + +async function openOptionalAbsoluteRealDirectory( + path: string, + label: string, +): Promise { + try { + return await openAbsoluteRealDirectory(path, label); + } catch (error) { + if (hasErrorCode(error, "ENOENT")) { + return null; + } + throw error; + } +} + +async function ensureNativeSkillAliasRoot( + sharedRootPath: string, + create: boolean, + signal: AbortSignal, +): Promise<{ directory: FileHandle; path: string } | null> { + signal.throwIfAborted(); + const sharedRoot = resolve(sharedRootPath); + const aliasRoot = join(sharedRoot, ".agents", "skills"); + const sharedRootDirectory = await openAbsoluteRealDirectory(sharedRoot, "Session shared root"); + let agentsRootDirectory: FileHandle | null = null; + let aliasDirectory: FileHandle | null = null; + + try { + agentsRootDirectory = create + ? await ensureRealDirectoryAt( + sharedRootDirectory, + ".agents", + "Native skill alias root", + signal, + ) + : await openOptionalRealDirectory( + directoryEntryPath(sharedRootDirectory, ".agents"), + "Native skill alias root", + ); + if (agentsRootDirectory !== null) { + aliasDirectory = create + ? await ensureRealDirectoryAt( + agentsRootDirectory, + "skills", + "Native skill alias root", + signal, + ) + : await openOptionalRealDirectory( + directoryEntryPath(agentsRootDirectory, "skills"), + "Native skill alias root", + ); + } + } catch (error) { + const closeFailures = await closeFileHandles([ + aliasDirectory, + agentsRootDirectory, + sharedRootDirectory, + ]); + if (closeFailures.length > 0) { + throw new AggregateError( + [error, ...closeFailures], + "Failed to open native skill alias root.", + ); + } + throw error; + } + + const closeFailures = await closeFileHandles([agentsRootDirectory, sharedRootDirectory]); + if (closeFailures.length > 0) { + const aliasCloseFailures = await closeFileHandles([aliasDirectory]); + throw new AggregateError( + [...closeFailures, ...aliasCloseFailures], + "Failed to close native skill alias ancestors.", + ); + } + return aliasDirectory === null ? null : { directory: aliasDirectory, path: aliasRoot }; +} + +function resolveMaterializedSkillMount(sharedRootPath: string, mountPath: string): string { + const resolvedMount = resolve(mountPath); + if (dirname(resolvedMount) !== resolve(sharedRootPath, ".mosoo", "skill")) { + throw new Error(`Resolved skill mount path is outside the allowed root: ${mountPath}.`); + } + return resolvedMount; +} + +function isManagedNativeSkillAliasTarget(sharedRootPath: string, target: string): boolean { + return dirname(resolve(target)) === resolve(sharedRootPath, ".mosoo", "skill"); +} + +export async function exposeNativeSkillAliases( + execution: DriverExecutionInput, + logger: Logger, + materializedSkills: readonly AgentDriverMaterializedSkill[], + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + const desired = new Map(); + + for (const skill of materializedSkills) { + signal.throwIfAborted(); + if (skill.skillName.length > 64 || !NATIVE_SKILL_NAME_PATTERN.test(skill.skillName)) { + logger.warn("driver.skill.native_alias.skipped", { + reason: "invalid_name", + skillId: skill.skillId, + skillName: skill.skillName, + }); + continue; + } + if (desired.has(skill.skillName)) { + throw new Error(`Materialized skills contain a duplicate skill name: ${skill.skillName}.`); + } + + const mountPath = resolveMaterializedSkillMount( + execution.session.sharedRootPath, + skill.mountPath, + ); + if (resolve(skill.skillMarkdownPath) !== join(mountPath, "SKILL.md")) { + throw new Error(`Native skill path mismatch for "${skill.skillName}".`); + } + desired.set(skill.skillName, skill); + } + + const aliasRoot = await ensureNativeSkillAliasRoot( + execution.session.sharedRootPath, + desired.size > 0, + signal, + ); + if (aliasRoot === null) { + return []; + } + + const retainedAliases = new Set(); + let result: string[] | null = null; + let operationError: unknown = null; + try { + const entries = await readDirectoryEntriesBounded( + aliasRoot.directory, + "Native skill alias root", + MAX_NATIVE_SKILL_ALIAS_ENTRIES, + signal, + ); + for (const entry of entries) { + signal.throwIfAborted(); + const aliasPath = directoryEntryPath(aliasRoot.directory, entry.name); + const skill = desired.get(entry.name); + + if (entry.isSymbolicLink()) { + const target = resolve(aliasRoot.path, await readlink(aliasPath)); + if (skill !== undefined && target === resolve(skill.mountPath)) { + retainedAliases.add(entry.name); + continue; + } + if (isManagedNativeSkillAliasTarget(execution.session.sharedRootPath, target)) { + await unlink(aliasPath); + continue; + } + } + if (skill !== undefined) { + throw new Error(`Native skill alias "${entry.name}" collides with an existing path.`); + } + } + + result = []; + for (const [skillName, skill] of desired) { + signal.throwIfAborted(); + result.push(join(aliasRoot.path, skillName)); + if (!retainedAliases.has(skillName)) { + await symlink( + relative(aliasRoot.path, resolve(skill.mountPath)), + directoryEntryPath(aliasRoot.directory, skillName), + "dir", + ); + } + } + await aliasRoot.directory.sync(); + signal.throwIfAborted(); + await assertDirectoryIdentity(aliasRoot.directory, aliasRoot.path, "Native skill alias root"); + } catch (error) { + operationError = error; + } + + const closeFailures = await closeFileHandles([aliasRoot.directory]); + if (operationError !== null) { + if (closeFailures.length > 0) { + throw new AggregateError( + [operationError, ...closeFailures], + "Native skill alias update and cleanup failed.", + ); + } + throw operationError; + } + if (closeFailures.length > 0) { + throw new AggregateError(closeFailures, "Failed to close the native skill alias root."); + } + if (result === null) { + throw new Error("Native skill aliases completed without a result."); + } + return result; +} + +async function ensureSkillCatalogRoot( + execution: DriverExecutionInput, + signal: AbortSignal, +): Promise<{ directory: FileHandle; path: string }> { + signal.throwIfAborted(); + const sharedRoot = resolve(execution.session.sharedRootPath); + const mosooRoot = join(sharedRoot, ".mosoo"); + const skillCatalogRoot = join(mosooRoot, "skills"); + const sharedRootDirectory = await openAbsoluteRealDirectory(sharedRoot, "Session shared root"); + let mosooRootDirectory: FileHandle | null = null; + let skillCatalogRootDirectory: FileHandle | null = null; + let operationError: unknown = null; + + try { + mosooRootDirectory = await ensureRealDirectoryAt( + sharedRootDirectory, + ".mosoo", + "Skill bootstrap .mosoo root", + signal, + ); + skillCatalogRootDirectory = await ensureRealDirectoryAt( + mosooRootDirectory, + "skills", + "Skill bootstrap catalog root", + signal, + ); + } catch (error) { + operationError = error; + } + + const ancestorCloseFailures = await closeFileHandles([mosooRootDirectory, sharedRootDirectory]); + if (operationError !== null || ancestorCloseFailures.length > 0) { + const catalogCloseFailures = await closeFileHandles([skillCatalogRootDirectory]); + const failures = [...ancestorCloseFailures, ...catalogCloseFailures]; + if (operationError !== null) { + throw failures.length > 0 + ? new AggregateError([operationError, ...failures], "Failed to open skill catalog root.") + : operationError; + } + throw new AggregateError(failures, "Failed to close skill catalog ancestors."); + } + if (skillCatalogRootDirectory === null) { + throw new Error("Skill catalog root was not opened."); + } + return { directory: skillCatalogRootDirectory, path: skillCatalogRoot }; +} + +async function unlinkManagedFile( + directory: FileHandle, + name: string, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + try { + await unlink(directoryEntryPath(directory, name)); + } catch (error) { + if (!hasErrorCode(error, "ENOENT")) { + throw error; + } + } +} + +async function removeSkillBootstrapArtifacts( + execution: DriverExecutionInput, + signal: AbortSignal, +): Promise { + const sharedRoot = resolve(execution.session.sharedRootPath); + const sharedRootDirectory = await openOptionalAbsoluteRealDirectory( + sharedRoot, + "Session shared root", + ); + if (sharedRootDirectory === null) { + return; + } + let mosooRootDirectory: FileHandle | null = null; + let skillCatalogRootDirectory: FileHandle | null = null; + let operationError: unknown = null; + + try { + mosooRootDirectory = await openOptionalRealDirectory( + directoryEntryPath(sharedRootDirectory, ".mosoo"), + "Skill bootstrap .mosoo root", + ); + if (mosooRootDirectory !== null) { + skillCatalogRootDirectory = await openOptionalRealDirectory( + directoryEntryPath(mosooRootDirectory, "skills"), + "Skill bootstrap catalog root", + ); + } + if (skillCatalogRootDirectory !== null) { + await cleanupAtomicWriteTemporaryFiles( + skillCatalogRootDirectory, + ["manifest.json", "README.md"], + signal, + ); + await unlinkManagedFile(skillCatalogRootDirectory, "manifest.json", signal); + await unlinkManagedFile(skillCatalogRootDirectory, "README.md", signal); + await skillCatalogRootDirectory.sync(); + await assertDirectoryIdentity( + skillCatalogRootDirectory, + getSkillCatalogRoot(execution), + "Skill bootstrap catalog root", + ); + } + } catch (error) { + operationError = error; + } + + const closeFailures = await closeFileHandles([ + skillCatalogRootDirectory, + mosooRootDirectory, + sharedRootDirectory, + ]); + if (operationError !== null) { + if (closeFailures.length > 0) { + throw new AggregateError( + [operationError, ...closeFailures], + "Skill bootstrap removal and cleanup failed.", + ); + } + throw operationError; + } + if (closeFailures.length > 0) { + throw new AggregateError(closeFailures, "Failed to close skill bootstrap roots."); + } +} + function getSkillCatalogManifestEntries( execution: DriverExecutionInput, + materializedSkills: readonly AgentDriverMaterializedSkill[], ): SkillCatalogManifestEntry[] { - return execution.skillCatalog.map((entry) => ({ - frontmatter: entry.frontmatter, - mountPath: entry.mountPath, - resolutionMode: entry.resolutionMode, - skillId: entry.skillId, - skillMarkdownPath: join(entry.mountPath, "SKILL.md"), - skillName: entry.skillName, - })); -} - -function buildSkillCatalogReadme(execution: DriverExecutionInput): string { - const manifestEntries = getSkillCatalogManifestEntries(execution); + const materializedById = new Map(materializedSkills.map((skill) => [skill.skillId, skill])); + const matchedSkillIds = new Set(); + const entries = execution.skillCatalog.map((entry) => { + if (entry.resolutionMode === "tombstone") { + return { + frontmatter: entry.frontmatter, + mountPath: entry.mountPath, + resolutionMode: entry.resolutionMode, + skillId: entry.skillId, + skillMarkdownPath: join(entry.mountPath, "SKILL.md"), + skillName: entry.skillName, + }; + } + + const materialized = materializedById.get(entry.skillId); + if ( + materialized === undefined || + materialized.skillName !== entry.skillName || + resolve(materialized.mountPath) !== resolve(entry.mountPath) + ) { + throw new Error(`Skill catalog does not match materialized skill ${entry.skillId}.`); + } + matchedSkillIds.add(entry.skillId); + + return { + frontmatter: entry.frontmatter, + mountPath: materialized.mountPath, + resolutionMode: entry.resolutionMode, + skillId: materialized.skillId, + skillMarkdownPath: materialized.skillMarkdownPath, + skillName: materialized.skillName, + }; + }); + + if (matchedSkillIds.size !== materializedSkills.length) { + throw new Error("Skill catalog does not match the materialized skill set."); + } + + return entries; +} + +function buildSkillCatalogReadme(manifestEntries: readonly SkillCatalogManifestEntry[]): string { const lines = [ "# Skill Catalog", "", @@ -72,56 +463,67 @@ function buildSkillCatalogReadme(execution: DriverExecutionInput): string { export async function writeSkillBootstrapArtifacts( execution: DriverExecutionInput, + materializedSkills: readonly AgentDriverMaterializedSkill[], + signal: AbortSignal, ): Promise { + signal.throwIfAborted(); + const manifestEntries = getSkillCatalogManifestEntries(execution, materializedSkills); + if (execution.skillCatalog.length === 0) { + await removeSkillBootstrapArtifacts(execution, signal); return null; } - const skillCatalogRoot = getSkillCatalogRoot(execution); - const manifestPath = join(skillCatalogRoot, "manifest.json"); - const readmePath = join(skillCatalogRoot, "README.md"); + const skillCatalogRoot = await ensureSkillCatalogRoot(execution, signal); + await using skillCatalogDirectory = skillCatalogRoot.directory; + const manifestPath = join(skillCatalogRoot.path, "manifest.json"); + const readmePath = join(skillCatalogRoot.path, "README.md"); - await mkdir(skillCatalogRoot, { recursive: true }); - await writeFile( - manifestPath, - JSON.stringify(getSkillCatalogManifestEntries(execution), null, 2), - "utf8", + await cleanupAtomicWriteTemporaryFiles( + skillCatalogDirectory, + ["manifest.json", "README.md"], + signal, ); - await writeFile(readmePath, buildSkillCatalogReadme(execution), "utf8"); - - return { - manifestPath, - readmePath, - }; -} - -export function buildRuntimeBootstrapText(execution: DriverExecutionInput): string { - const systemPrompt = execution.systemPrompt.trim(); - const manifestPath = join(getSkillCatalogRoot(execution), "manifest.json"); - const readmePath = join(getSkillCatalogRoot(execution), "README.md"); - const availableSkills = execution.skillCatalog.filter( - (entry) => entry.resolutionMode !== "tombstone", + await writeFileAtomically( + skillCatalogDirectory, + "manifest.json", + JSON.stringify(manifestEntries, null, 2), + 0o644, + signal, ); - const unavailableSkills = execution.skillCatalog.filter( - (entry) => entry.resolutionMode === "tombstone", + await writeFileAtomically( + skillCatalogDirectory, + "README.md", + buildSkillCatalogReadme(manifestEntries), + 0o644, + signal, + ); + await assertDirectoryIdentity( + skillCatalogDirectory, + skillCatalogRoot.path, + "Skill bootstrap catalog root", ); - if (!systemPrompt && execution.skillCatalog.length === 0) { - return ""; - } + return { manifestPath, readmePath }; +} - const sections = [ - "Internal runtime bootstrap for this session.", - "Record these instructions for future turns.", - "Do not treat this as an end-user request.", - "Do not ask follow-up questions, do not call tools, and do not modify files in response to this bootstrap message.", - ]; +function buildRuntimeContextSections(execution: DriverExecutionInput): string[] { + const systemPrompt = execution.systemPrompt.trim(); + const sections: string[] = []; if (systemPrompt) { sections.push(`Agent profile prompt:\n${systemPrompt}`); } if (execution.skillCatalog.length > 0) { + const manifestPath = join(getSkillCatalogRoot(execution), "manifest.json"); + const readmePath = join(getSkillCatalogRoot(execution), "README.md"); + const availableSkills = execution.skillCatalog.filter( + (entry) => entry.resolutionMode !== "tombstone", + ); + const unavailableSkills = execution.skillCatalog.filter( + (entry) => entry.resolutionMode === "tombstone", + ); const skillLines = [ `Skill catalog README: ${readmePath}`, `Skill catalog manifest: ${manifestPath}`, @@ -148,37 +550,78 @@ export function buildRuntimeBootstrapText(execution: DriverExecutionInput): stri sections.push(skillLines.join("\n")); } - sections.push("Reply with exactly READY."); - return sections.join("\n\n"); + return sections; +} + +export function buildRuntimeBootstrapText(execution: DriverExecutionInput): string { + const contextSections = buildRuntimeContextSections(execution); + + if (contextSections.length === 0) { + return ""; + } + + return [ + "Internal runtime bootstrap for this session.", + "Record these instructions for future turns.", + "Do not treat this as an end-user request.", + "Do not ask follow-up questions, do not call tools, and do not modify files in response to this bootstrap message.", + ...contextSections, + "Reply with exactly READY.", + ].join("\n\n"); } export function buildNativeRuntimeSystemPrompt(execution: DriverExecutionInput): string | null { - const bootstrap = buildRuntimeBootstrapText(execution) - .replace("Internal runtime bootstrap for this session.", "Runtime context for this session.") - .replace("Record these instructions for future turns.", "") - .replace("Do not treat this as an end-user request.", "") - .replace( - "Do not ask follow-up questions, do not call tools, and do not modify files in response to this bootstrap message.", - "", - ) - .replace("Reply with exactly READY.", "") - .trim(); + const contextSections = buildRuntimeContextSections(execution); - return bootstrap.length > 0 ? bootstrap : null; + return contextSections.length > 0 + ? ["Runtime context for this session.", ...contextSections].join("\n\n") + : null; } export async function writeNativeRuntimeSystemPrompt( execution: DriverExecutionInput, + materializedSkills: readonly AgentDriverMaterializedSkill[], + signal: AbortSignal, ): Promise { + signal.throwIfAborted(); + getSkillCatalogManifestEntries(execution, materializedSkills); const systemPrompt = buildNativeRuntimeSystemPrompt(execution); + const path = join(execution.session.homePath, "runtime-instructions.md"); if (systemPrompt === null) { + const homeDirectory = await openOptionalAbsoluteRealDirectory( + execution.session.homePath, + "Runtime home", + ); + if (homeDirectory === null) { + return null; + } + await using ownedHomeDirectory = homeDirectory; + await cleanupAtomicWriteTemporaryFiles(ownedHomeDirectory, ["runtime-instructions.md"], signal); + await unlinkManagedFile(ownedHomeDirectory, "runtime-instructions.md", signal); + await ownedHomeDirectory.sync(); + await assertDirectoryIdentity( + ownedHomeDirectory, + resolve(execution.session.homePath), + "Runtime home", + ); return null; } - const path = join(execution.session.homePath, "runtime-instructions.md"); - await mkdir(execution.session.homePath, { recursive: true }); - await writeFile(path, `${systemPrompt}\n`, { encoding: "utf8", mode: 0o600 }); + await using homeDirectory = await ensureAbsoluteRealDirectory( + execution.session.homePath, + "Runtime home", + signal, + ); + await cleanupAtomicWriteTemporaryFiles(homeDirectory, ["runtime-instructions.md"], signal); + await writeFileAtomically( + homeDirectory, + "runtime-instructions.md", + `${systemPrompt}\n`, + 0o600, + signal, + ); + await assertDirectoryIdentity(homeDirectory, resolve(execution.session.homePath), "Runtime home"); return path; } diff --git a/src/runtimes/skill-materialization.ts b/src/runtimes/skill-materialization.ts index 1889421..e4df9b4 100644 --- a/src/runtimes/skill-materialization.ts +++ b/src/runtimes/skill-materialization.ts @@ -1,17 +1,8 @@ -import { createHash } from "node:crypto"; -import { - chmod, - lstat, - mkdir, - readFile, - readdir, - readlink, - rm, - symlink, - unlink, - writeFile, -} from "node:fs/promises"; -import { dirname, join, relative, resolve } from "node:path"; +import { createHash, randomUUID } from "node:crypto"; +import { constants } from "node:fs"; +import { lstat, mkdir, open, opendir, rename, rmdir, unlink } from "node:fs/promises"; +import type { FileHandle } from "node:fs/promises"; +import { basename, dirname, join, resolve } from "node:path"; import type { AgentDriverMaterializedSkill } from "../host-ports"; import type { Logger } from "../observability"; @@ -19,297 +10,1238 @@ import type { DriverResolvedSkill } from "../protocol/boot"; import type { DriverExecutionInput } from "../protocol/execution"; import { extractZipArchive } from "../skill-package"; import type { SkillArchiveExtractOptions, SkillPackageEntry } from "../skill-package"; +import { + assertDirectoryIdentity, + closeFileHandles, + directoryEntryPath, + ensureRealDirectoryAt, + openedDirectoryPath, + openAbsoluteRealDirectory, + openRealDirectory, + readDirectoryEntriesBounded, +} from "./atomic-file"; -export type MaterializedSkill = AgentDriverMaterializedSkill; - +const MAX_SKILL_COMPRESSED_BYTES = 25 * 1024 * 1024; const MAX_SKILL_ENTRY_BYTES = 2 * 1024 * 1024; const MAX_SKILL_UNCOMPRESSED_BYTES = 25 * 1024 * 1024; -const NATIVE_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +const SKILL_DOWNLOAD_TIMEOUT_MS = 30_000; const SKILL_ARCHIVE_EXTRACT_OPTIONS: SkillArchiveExtractOptions = { maxEntryCount: 256, maxFileBytes: MAX_SKILL_ENTRY_BYTES, maxTotalFileBytes: MAX_SKILL_UNCOMPRESSED_BYTES, }; -interface SkillMaterializationMarker { - readonly blobSha256: string; +interface MaterializationRoots { + readonly mosooDirectory: FileHandle; + readonly mosooRoot: string; + readonly mountRoot: string; + readonly transactionDirectory: FileHandle; + readonly transactionRoot: string; +} + +interface ResolvedSkillInput { + readonly mountPath: string; + readonly skill: DriverResolvedSkill; readonly snapshotId: string; } -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); +interface MaterializationOwner { + currentName: string; + readonly newDirectory: FileHandle | null; + readonly ownerDirectory: FileHandle; } -function parseSkillMaterializationMarker(value: unknown): SkillMaterializationMarker | null { - if (!isRecord(value) || typeof value["blobSha256"] !== "string") { - return null; - } +interface QuarantineCleanupBudget { + readonly deadlineMs: number; + remainingEntries: number; +} - if (typeof value["snapshotId"] !== "string") { - return null; - } +interface QuarantineCleanupFrame { + readonly directory: Awaited>; + readonly handle: FileHandle; + readonly removePath: string; +} - return { - blobSha256: value["blobSha256"], - snapshotId: value["snapshotId"], +const latestGenerationByRoot = new Map(); +const activeStagingPaths = new Set(); + +const SKILL_STAGING_DIRECTORY_PATTERN = + /^stage-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const SKILL_QUARANTINE_DIRECTORY_PATTERN = + /^quarantine-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const MAX_MANAGED_DIRECTORY_ENTRIES = 1_024; +const SKILL_QUARANTINE_CLEANUP_BUDGET_MS = 250; +const MAX_SKILL_QUARANTINE_CLEANUP_ENTRIES = 1_024; +const MAX_SKILL_QUARANTINE_CLEANUP_DEPTH = 64; +const SKILL_TRANSACTION_ACTIVE_NAME = "active"; +const SKILL_TRANSACTION_COMMIT_MARKER_NAME = "COMMITTED"; + +function createSerialLock(): (operation: () => Promise) => Promise { + let tail: Promise = Promise.resolve(); + return async (operation: () => Promise): Promise => { + const predecessor = tail; + const release = Promise.withResolvers(); + tail = release.promise; + await predecessor; + try { + return await operation(); + } finally { + release.resolve(); + } }; } -function enforceSkillMountPath(sessionOrganizationPath: string, mountPath: string): void { +// ponytail: startup-only materialization is serialized process-wide until contention is measured. +const withCommitLock = createSerialLock(); +const withCleanupLock = createSerialLock(); + +function hasErrorCode(error: unknown, code: string): boolean { + return typeof error === "object" && error !== null && "code" in error && error.code === code; +} + +function enforceSkillMountPath(sessionOrganizationPath: string, mountPath: string): string { const allowedRoot = resolve(sessionOrganizationPath, ".mosoo", "skill"); const resolvedMountPath = resolve(mountPath); - const relativeMountPath = relative(allowedRoot, resolvedMountPath); - if ( - relativeMountPath.length === 0 || - relativeMountPath.startsWith("..") || - relativeMountPath.includes("/") - ) { + if (dirname(resolvedMountPath) !== allowedRoot) { throw new Error(`Resolved skill mount path is outside the allowed root: ${mountPath}.`); } + + return resolvedMountPath; } -async function ensureNativeSkillDirectory(path: string): Promise { +async function readPathStats(path: string): Promise> | null> { try { - const stats = await lstat(path); - - if (!stats.isDirectory()) { - throw new Error(`Native skill alias root is not a directory: ${path}.`); + return await lstat(path); + } catch (error) { + if (hasErrorCode(error, "ENOENT")) { + return null; } + + throw error; + } +} + +async function openOptionalRealDirectory(path: string, label: string): Promise { + try { + return await openRealDirectory(path, label); } catch (error) { - if (isRecord(error) && error["code"] === "ENOENT") { - await mkdir(path); - return; + if (hasErrorCode(error, "ENOENT")) { + return null; } + throw error; + } +} +async function ensureMaterializationRoots( + sessionOrganizationPath: string, + create: boolean, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + const sharedRoot = resolve(sessionOrganizationPath); + const mosooRoot = join(sharedRoot, ".mosoo"); + const mountRoot = join(mosooRoot, "skill"); + const transactionRoot = join(mosooRoot, ".skill-transactions"); + const sharedRootDirectory = await openAbsoluteRealDirectory(sharedRoot, "Session shared root"); + let mosooRootDirectory: FileHandle | null = null; + let transactionDirectory: FileHandle | null = null; + let result: MaterializationRoots | null = null; + + try { + mosooRootDirectory = create + ? await ensureRealDirectoryAt(sharedRootDirectory, ".mosoo", "Session .mosoo root", signal) + : await openOptionalRealDirectory( + directoryEntryPath(sharedRootDirectory, ".mosoo"), + "Session .mosoo root", + ); + if (mosooRootDirectory === null) { + result = null; + } else { + transactionDirectory = await openOptionalRealDirectory( + directoryEntryPath(mosooRootDirectory, ".skill-transactions"), + "Skill transaction root", + ); + if ( + create || + transactionDirectory !== null || + (await readPathStats(directoryEntryPath(mosooRootDirectory, "skill"))) !== null + ) { + transactionDirectory ??= await ensureRealDirectoryAt( + mosooRootDirectory, + ".skill-transactions", + "Skill transaction root", + signal, + ); + result = { + mosooDirectory: mosooRootDirectory, + mosooRoot, + mountRoot, + transactionDirectory, + transactionRoot, + }; + } + } + } catch (error) { + const closeFailures = await closeFileHandles([ + transactionDirectory, + mosooRootDirectory, + sharedRootDirectory, + ]); + if (closeFailures.length > 0) { + throw new AggregateError([error, ...closeFailures], "Failed to open skill roots."); + } throw error; } + + const closeFailures = await closeFileHandles([ + result === null ? mosooRootDirectory : null, + sharedRootDirectory, + ]); + if (closeFailures.length > 0) { + const ownedCloseFailures = + result === null + ? [] + : await closeFileHandles([result.transactionDirectory, result.mosooDirectory]); + throw new AggregateError( + [...closeFailures, ...ownedCloseFailures], + "Failed to close skill root ancestors.", + ); + } + return result; +} + +async function assertMaterializationRootIdentities(roots: MaterializationRoots): Promise { + await assertDirectoryIdentity(roots.mosooDirectory, roots.mosooRoot, "Session .mosoo root"); + await assertDirectoryIdentity( + roots.transactionDirectory, + roots.transactionRoot, + "Skill transaction root", + ); +} + +async function assertSkillMountLeaf(mountPath: string): Promise { + const stats = await readPathStats(mountPath); + + if (stats !== null && (stats.isSymbolicLink() || !stats.isDirectory())) { + throw new Error(`Resolved skill mount must be a real directory or absent: ${mountPath}.`); + } +} + +function createQuarantineCleanupBudget(): QuarantineCleanupBudget { + return { + deadlineMs: Date.now() + SKILL_QUARANTINE_CLEANUP_BUDGET_MS, + remainingEntries: MAX_SKILL_QUARANTINE_CLEANUP_ENTRIES, + }; } -function isManagedNativeSkillAliasTarget(sharedRootPath: string, target: string): boolean { +async function openQuarantineCleanupFrame(removePath: string): Promise { + const handle = await openRealDirectory(removePath, "Skill quarantine directory"); + try { - enforceSkillMountPath(sharedRootPath, target); + return { + directory: await opendir(openedDirectoryPath(handle)), + handle, + removePath, + }; + } catch (error) { + const closeFailures = await closeFileHandles([handle]); + if (closeFailures.length > 0) { + throw new AggregateError( + [error, ...closeFailures], + "Failed to open a skill quarantine directory.", + ); + } + throw error; + } +} + +async function closeQuarantineCleanupFrames( + frames: readonly QuarantineCleanupFrame[], +): Promise { + const results = await Promise.allSettled( + frames.flatMap((frame) => [frame.directory.close(), frame.handle.close()]), + ); + return results.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])); +} + +async function cleanupQuarantine( + path: string, + budget: QuarantineCleanupBudget, + transactionDirectory: FileHandle, +): Promise { + const rootStats = await readPathStats(path); + + if (rootStats === null) { + return true; + } + if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) { + await unlink(path); return true; - } catch { - return false; } + + const frames: QuarantineCleanupFrame[] = [await openQuarantineCleanupFrame(path)]; + let complete = true; + let operationError: unknown = null; + + try { + while (frames.length > 0) { + if (budget.remainingEntries <= 0 || Date.now() >= budget.deadlineMs) { + complete = false; + break; + } + + const frame = frames[frames.length - 1]; + if (frame === undefined) { + throw new Error("Skill quarantine traversal lost its active directory."); + } + const entry = await frame.directory.read(); + + if (entry === null) { + frames.pop(); + const closeFailures = await closeQuarantineCleanupFrames([frame]); + if (closeFailures.length > 0) { + throw new AggregateError(closeFailures, "Failed to close skill quarantine handles."); + } + budget.remainingEntries -= 1; + try { + await rmdir(frame.removePath); + } catch (error) { + if (hasErrorCode(error, "ENOENT")) { + continue; + } + if (hasErrorCode(error, "ENOTEMPTY")) { + complete = false; + break; + } + throw error; + } + continue; + } + + budget.remainingEntries -= 1; + const entryPath = directoryEntryPath(frame.handle, entry.name); + const stats = await readPathStats(entryPath); + if (stats === null) { + continue; + } + if (stats.isDirectory() && !stats.isSymbolicLink()) { + if (frames.length >= MAX_SKILL_QUARANTINE_CLEANUP_DEPTH) { + await rename( + entryPath, + directoryEntryPath(transactionDirectory, `quarantine-${randomUUID()}`), + ); + await transactionDirectory.sync(); + complete = false; + continue; + } + frames.push(await openQuarantineCleanupFrame(entryPath)); + } else { + await unlink(entryPath); + } + } + } catch (error) { + operationError = error; + } + + const closeFailures = await closeQuarantineCleanupFrames(frames); + if (operationError !== null) { + if (closeFailures.length > 0) { + throw new AggregateError( + [operationError, ...closeFailures], + "Skill quarantine cleanup and resource cleanup failed.", + ); + } + throw operationError; + } + if (closeFailures.length > 0) { + throw new AggregateError(closeFailures, "Failed to close skill quarantine handles."); + } + return complete; } -export async function exposeNativeSkillAliases( +export async function materializeResolvedSkills( execution: DriverExecutionInput, logger: Logger, - materializedSkills: readonly MaterializedSkill[], -): Promise { - const desired = new Map(); - - for (const skill of materializedSkills) { - if (skill.skillName.length > 64 || !NATIVE_SKILL_NAME_PATTERN.test(skill.skillName)) { - logger.warn("driver.skill.native_alias.skipped", { - reason: "invalid_name", - skillId: skill.skillId, - skillName: skill.skillName, - }); - continue; + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + const inputs = resolveSkillInputs(execution, logger); + const rootKey = resolve(execution.session.sharedRootPath); + const generation = new AbortController(); + const superseded = new Error("Skill materialization was superseded by a newer generation."); + + await withCommitLock(async () => { + signal.throwIfAborted(); + latestGenerationByRoot.get(rootKey)?.abort(superseded); + latestGenerationByRoot.set(rootKey, generation); + }); + + const operationSignal = AbortSignal.any([signal, generation.signal]); + let roots: MaterializationRoots | null = null; + let result: AgentDriverMaterializedSkill[] | null = null; + let materializationError: unknown = null; + try { + roots = await ensureMaterializationRoots( + execution.session.sharedRootPath, + inputs.length > 0, + operationSignal, + ); + if (roots === null) { + result = []; + } else { + result = await materializeSkillGeneration( + logger, + inputs, + roots, + generation.signal, + operationSignal, + ); + } + } catch (error) { + materializationError = error; + } + + if (latestGenerationByRoot.get(rootKey) === generation) { + latestGenerationByRoot.delete(rootKey); + } + const closeFailures = + roots === null + ? [] + : await closeFileHandles([roots.transactionDirectory, roots.mosooDirectory]); + if (materializationError !== null) { + if (closeFailures.length > 0) { + throw new AggregateError( + [materializationError, ...closeFailures], + "Skill materialization and root cleanup failed.", + ); + } + throw materializationError; + } + if (closeFailures.length > 0) { + throw new AggregateError(closeFailures, "Failed to close skill materialization roots."); + } + if (result === null) { + throw new Error("Skill materialization completed without a result."); + } + return result; +} + +async function materializeSkillGeneration( + logger: Logger, + inputs: readonly ResolvedSkillInput[], + roots: MaterializationRoots, + generationSignal: AbortSignal, + operationSignal: AbortSignal, +): Promise { + let liveCatalogExists = false; + await withCommitLock(async () => { + operationSignal.throwIfAborted(); + await withCleanupLock(async () => recoverSkillTransactions(roots, logger, operationSignal)); + await assertMaterializationRootIdentities(roots); + await assertDesiredSkillMounts(roots, inputs); + liveCatalogExists = + (await readPathStats(directoryEntryPath(roots.mosooDirectory, "skill"))) !== null; + }); + + if (inputs.length === 0 && !liveCatalogExists) { + operationSignal.throwIfAborted(); + return []; + } + + const owner = await createMaterializationOwner(roots, operationSignal); + let result: AgentDriverMaterializedSkill[] | null = null; + let materializationError: unknown = null; + try { + for (const input of inputs) { + await prepareSkill(input, owner, operationSignal); + } + + await withCommitLock(async () => { + operationSignal.throwIfAborted(); + await assertMaterializationRootIdentities(roots); + await assertDesiredSkillMounts(roots, inputs); + await commitSkillTransaction(owner, roots, operationSignal); + }); + generationSignal.throwIfAborted(); + await assertMaterializationRootIdentities(roots); + result = inputs.map(({ mountPath, skill, snapshotId }) => ({ + mountPath, + skillId: skill.skillId, + skillMarkdownPath: join(mountPath, "SKILL.md"), + skillName: skill.skillName, + snapshotId, + })); + } catch (error) { + materializationError = error; + } + + const cleanupFailures = await disposeMaterializationOwner(owner, roots, logger); + if (materializationError !== null) { + if (cleanupFailures.length > 0) { + throw new AggregateError( + [materializationError, ...cleanupFailures], + "Skill materialization cleanup failed.", + ); } - if (desired.has(skill.skillName)) { - logger.warn("driver.skill.native_alias.skipped", { - reason: "duplicate_name", + throw materializationError; + } + if (cleanupFailures.length > 0) { + throw new AggregateError(cleanupFailures, "Skill materialization cleanup failed."); + } + if (result === null) { + throw new Error("Skill generation completed without a result."); + } + return result; +} + +function resolveSkillInputs(execution: DriverExecutionInput, logger: Logger): ResolvedSkillInput[] { + const inputs: ResolvedSkillInput[] = []; + const mounts = new Set(); + const names = new Set(); + + for (const skill of execution.skills) { + const snapshotId = skill.snapshotId; + + if ( + skill.resolutionMode === "tombstone" || + snapshotId === undefined || + snapshotId === null || + snapshotId.length === 0 + ) { + logger.info("driver.skill.skipped", { + reason: skill.warningCode ?? "skill.tombstone", skillId: skill.skillId, skillName: skill.skillName, }); continue; } - enforceSkillMountPath(execution.session.sharedRootPath, skill.mountPath); - if (resolve(skill.skillMarkdownPath) !== resolve(skill.mountPath, "SKILL.md")) { - throw new Error(`Native skill path mismatch for "${skill.skillName}".`); + const mountPath = enforceSkillMountPath(execution.session.sharedRootPath, skill.mountPath); + if (mounts.has(mountPath)) { + throw new Error(`Resolved skills contain a duplicate mount path: ${mountPath}.`); + } + if (names.has(skill.skillName)) { + throw new Error(`Resolved skills contain a duplicate skill name: ${skill.skillName}.`); } - desired.set(skill.skillName, skill); + mounts.add(mountPath); + names.add(skill.skillName); + inputs.push({ mountPath, skill, snapshotId }); } - for (const skill of desired.values()) { - const stats = await lstat(skill.skillMarkdownPath); + validateActiveSkillCatalog(execution, inputs); + return inputs; +} + +function validateActiveSkillCatalog( + execution: DriverExecutionInput, + inputs: readonly ResolvedSkillInput[], +): void { + const activeCatalog = new Map(); + const catalogIds = new Set(); + const catalogNames = new Set(); + const catalogMounts = new Set(); + + for (const entry of execution.skillCatalog) { + const mountPath = enforceSkillMountPath(execution.session.sharedRootPath, entry.mountPath); + if (catalogIds.has(entry.skillId)) { + throw new Error(`Skill catalog contains a duplicate skill ID: ${entry.skillId}.`); + } + if (catalogNames.has(entry.skillName)) { + throw new Error(`Skill catalog contains a duplicate skill name: ${entry.skillName}.`); + } + if (catalogMounts.has(mountPath)) { + throw new Error(`Skill catalog contains a duplicate mount path: ${mountPath}.`); + } - if (!stats.isFile()) { - throw new Error(`Native skill "${skill.skillName}" does not contain SKILL.md.`); + catalogIds.add(entry.skillId); + catalogNames.add(entry.skillName); + catalogMounts.add(mountPath); + if (entry.resolutionMode !== "tombstone") { + activeCatalog.set(entry.skillId, entry); } } - const agentsRoot = join(execution.session.sharedRootPath, ".agents"); - const aliasRoot = join(agentsRoot, "skills"); - await ensureNativeSkillDirectory(agentsRoot); - await ensureNativeSkillDirectory(aliasRoot); + if (activeCatalog.size !== inputs.length) { + throw new Error("Active skill catalog does not match the resolved skill set."); + } - const staleAliases: string[] = []; - const retainedAliases = new Set(); - const entries = (await readdir(aliasRoot, { withFileTypes: true })).toSorted((a, b) => - a.name.localeCompare(b.name), - ); + for (const input of inputs) { + const entry = activeCatalog.get(input.skill.skillId); + if ( + entry === undefined || + entry.skillName !== input.skill.skillName || + resolve(entry.mountPath) !== input.mountPath || + entry.resolutionMode !== input.skill.resolutionMode + ) { + throw new Error( + `Active skill catalog entry does not match resolved skill ${input.skill.skillId}.`, + ); + } + } +} - for (const entry of entries) { - const aliasPath = join(aliasRoot, entry.name); - const skill = desired.get(entry.name); +async function assertDesiredSkillMounts( + roots: MaterializationRoots, + inputs: readonly ResolvedSkillInput[], +): Promise { + const mountPath = directoryEntryPath(roots.mosooDirectory, "skill"); + const mountDirectory = await openOptionalRealDirectory(mountPath, "Skill mount root"); + if (mountDirectory === null) { + return; + } + await using ownedMountDirectory = mountDirectory; + for (const input of inputs) { + await assertSkillMountLeaf(directoryEntryPath(ownedMountDirectory, basename(input.mountPath))); + } + await assertDirectoryIdentity(ownedMountDirectory, roots.mountRoot, "Skill mount root"); +} - if (entry.isSymbolicLink()) { - const target = resolve(aliasRoot, await readlink(aliasPath)); +async function createMaterializationOwner( + roots: MaterializationRoots, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + const transactionId = randomUUID(); + const currentName = `stage-${transactionId}`; + const currentPath = directoryEntryPath(roots.transactionDirectory, currentName); + const ownerKey = join(roots.transactionRoot, currentName); + let ownerDirectory: FileHandle | null = null; + let newDirectory: FileHandle | null = null; + activeStagingPaths.add(ownerKey); - if (skill !== undefined && target === resolve(skill.mountPath)) { - retainedAliases.add(entry.name); - continue; - } - if (isManagedNativeSkillAliasTarget(execution.session.sharedRootPath, target)) { - staleAliases.push(aliasPath); - continue; - } + try { + await mkdir(currentPath, { mode: 0o700 }); + ownerDirectory = await openRealDirectory(currentPath, "Skill transaction owner"); + newDirectory = await ensureRealDirectoryAt(ownerDirectory, "new", "New skill owner", signal); + await Promise.all([newDirectory.sync(), ownerDirectory.sync()]); + await roots.transactionDirectory.sync(); + return { + currentName, + newDirectory, + ownerDirectory, + }; + } catch (error) { + activeStagingPaths.delete(ownerKey); + const failures = await closeFileHandles([newDirectory, ownerDirectory]); + try { + await cleanupQuarantine( + currentPath, + createQuarantineCleanupBudget(), + roots.transactionDirectory, + ); + } catch (cleanupError) { + failures.push(cleanupError); } - - if (skill !== undefined) { - throw new Error(`Native skill alias "${entry.name}" collides with an existing path.`); + if (failures.length > 0) { + throw new AggregateError([error, ...failures], "Failed to create a skill transaction owner."); } + throw error; } +} - await Promise.all(staleAliases.map((aliasPath) => unlink(aliasPath))); +async function prepareSkill( + input: ResolvedSkillInput, + owner: MaterializationOwner, + signal: AbortSignal, +): Promise { + if (owner.newDirectory === null) { + throw new Error("New skill transaction root is missing."); + } + const downloaded = await downloadSkillPackage(input.skill, signal); - const aliasPaths: string[] = []; - for (const [skillName, skill] of desired) { - const aliasPath = join(aliasRoot, skillName); - aliasPaths.push(aliasPath); + signal.throwIfAborted(); + const entries = extractZipArchive(downloaded, SKILL_ARCHIVE_EXTRACT_OPTIONS); + if (!entries.some((entry) => entry.entryKind === "file" && entry.path === "SKILL.md")) { + throw new Error(`Skill package for ${input.skill.skillId} does not contain SKILL.md.`); + } - if (!retainedAliases.has(skillName)) { - await symlink(relative(aliasRoot, resolve(skill.mountPath)), aliasPath, "dir"); - } + signal.throwIfAborted(); + const stagingPath = directoryEntryPath(owner.newDirectory, basename(input.mountPath)); + await mkdir(stagingPath, { mode: 0o700 }); + await using stagingDirectory = await openRealDirectory(stagingPath, "Staged skill root"); + for (const entry of entries) { + await materializeSkillEntry(stagingDirectory, entry, signal); } + await syncSkillStageDirectories(stagingDirectory, entries, signal); + await owner.newDirectory.sync(); + await assertDirectoryIdentity(stagingDirectory, stagingPath, "Staged skill root"); - return aliasPaths; + signal.throwIfAborted(); + await using manifest = await open( + directoryEntryPath(stagingDirectory, "SKILL.md"), + constants.O_RDONLY | constants.O_NOFOLLOW, + ); + if (!(await manifest.stat()).isFile()) { + throw new Error(`Skill package for ${input.skill.skillId} has an invalid SKILL.md.`); + } } -export async function materializeResolvedSkills( - execution: DriverExecutionInput, - logger: Logger, -): Promise { - const materializedSkills = await Promise.all( - execution.skills.map((skill) => materializeResolvedSkill(execution, logger, skill)), +async function commitSkillTransaction( + owner: MaterializationOwner, + roots: MaterializationRoots, + signal: AbortSignal, +): Promise { + if (owner.newDirectory === null) { + throw new Error("New skill transaction root is missing."); + } + await Promise.all([owner.newDirectory.sync(), owner.ownerDirectory.sync()]); + await assertDirectoryIdentity( + owner.newDirectory, + directoryEntryPath(owner.ownerDirectory, "new"), + "New skill catalog root", ); + signal.throwIfAborted(); + await assertMaterializationRootIdentities(roots); + await renameMaterializationOwner(owner, roots, SKILL_TRANSACTION_ACTIVE_NAME); - return materializedSkills.filter((skill): skill is MaterializedSkill => skill !== null); -} - -async function readSkillMaterializationMarker( - markerPath: string, -): Promise { + let markerCreated = false; try { - return parseSkillMaterializationMarker(JSON.parse(await readFile(markerPath, "utf8"))); - } catch { - return null; + await moveLiveSkillCatalogToBackup(owner, roots); + signal.throwIfAborted(); + await assertMaterializationRootIdentities(roots); + await assertDirectoryIdentity( + owner.newDirectory, + directoryEntryPath(owner.ownerDirectory, "new"), + "New skill catalog root", + ); + if ((await readPathStats(directoryEntryPath(roots.mosooDirectory, "skill"))) !== null) { + throw new Error("Skill mount root changed during catalog commit."); + } + await using marker = await open( + directoryEntryPath(owner.ownerDirectory, SKILL_TRANSACTION_COMMIT_MARKER_NAME), + constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, + 0o600, + ); + markerCreated = true; + await marker.sync(); + await owner.ownerDirectory.sync(); + } catch (error) { + try { + if (markerCreated) { + await finishCommittedSkillTransaction(owner, roots); + await retireMaterializationOwner(owner, roots); + } else { + await rollbackSkillTransaction(owner, roots); + } + } catch (recoveryError) { + throw new AggregateError( + [error, recoveryError], + markerCreated + ? "Skill materialization committed but could not finish installing the catalog." + : "Skill materialization failed and could not be fully rolled back.", + ); + } + throw error; } + + await finishCommittedSkillTransaction(owner, roots); + await retireMaterializationOwner(owner, roots); } -async function materializeResolvedSkill( - execution: DriverExecutionInput, - logger: Logger, - skill: DriverResolvedSkill, -): Promise { - const snapshotId = skill.snapshotId; +async function moveLiveSkillCatalogToBackup( + owner: MaterializationOwner, + roots: MaterializationRoots, +): Promise { + const livePath = directoryEntryPath(roots.mosooDirectory, "skill"); + const oldPath = directoryEntryPath(owner.ownerDirectory, "old"); + const liveStats = await readPathStats(livePath); + if (liveStats === null) { + return; + } + if (liveStats.isSymbolicLink() || !liveStats.isDirectory()) { + throw new Error(`Skill mount root must be a real directory: ${roots.mountRoot}.`); + } + if ((await readPathStats(oldPath)) !== null) { + throw new Error("Skill transaction backup already exists."); + } + + await rename(livePath, oldPath); + await Promise.all([roots.mosooDirectory.sync(), owner.ownerDirectory.sync()]); + const oldStats = await readPathStats(oldPath); + if (oldStats === null || oldStats.dev !== liveStats.dev || oldStats.ino !== liveStats.ino) { + throw new Error("Skill mount root changed during catalog commit."); + } +} +async function rollbackSkillTransaction( + owner: MaterializationOwner, + roots: MaterializationRoots, +): Promise { + if (owner.newDirectory === null) { + throw new Error("Interrupted skill transaction lost its new catalog root."); + } + const livePath = directoryEntryPath(roots.mosooDirectory, "skill"); + const oldPath = directoryEntryPath(owner.ownerDirectory, "old"); + const [liveStats, oldStats] = await Promise.all([ + readPathStats(livePath), + readPathStats(oldPath), + ]); + if (liveStats !== null && oldStats !== null) { + throw new Error("Interrupted skill transaction has both live and old catalog roots."); + } if ( - skill.resolutionMode === "tombstone" || - snapshotId === undefined || - snapshotId === null || - snapshotId.length === 0 + (liveStats !== null && (liveStats.isSymbolicLink() || !liveStats.isDirectory())) || + (oldStats !== null && (oldStats.isSymbolicLink() || !oldStats.isDirectory())) ) { - logger.info("driver.skill.skipped", { - reason: skill.warningCode ?? "skill.tombstone", - skillId: skill.skillId, - skillName: skill.skillName, - }); - return null; + throw new Error("Interrupted skill catalog root must be a real directory."); + } + if (oldStats !== null) { + await rename(oldPath, livePath); + await Promise.all([roots.mosooDirectory.sync(), owner.ownerDirectory.sync()]); + } + await retireMaterializationOwner(owner, roots); +} + +async function finishCommittedSkillTransaction( + owner: MaterializationOwner, + roots: MaterializationRoots, +): Promise { + const livePath = directoryEntryPath(roots.mosooDirectory, "skill"); + const newPath = directoryEntryPath(owner.ownerDirectory, "new"); + const [liveStats, newStats] = await Promise.all([ + readPathStats(livePath), + readPathStats(newPath), + ]); + if (liveStats !== null && newStats !== null) { + throw new Error("Committed skill transaction has both live and new catalog roots."); + } + if (liveStats === null && newStats === null) { + throw new Error("Committed skill transaction lost its catalog root."); + } + if (liveStats !== null) { + if (liveStats.isSymbolicLink() || !liveStats.isDirectory()) { + throw new Error("Committed skill catalog root must be a real directory."); + } + return; } + if (newStats!.isSymbolicLink() || !newStats!.isDirectory()) { + throw new Error("New skill catalog root must be a real directory."); + } + await rename(newPath, livePath); + await Promise.all([roots.mosooDirectory.sync(), owner.ownerDirectory.sync()]); + if (owner.newDirectory !== null) { + await assertDirectoryIdentity(owner.newDirectory, roots.mountRoot, "Skill mount root"); + } +} - enforceSkillMountPath(execution.session.sharedRootPath, skill.mountPath); - const skillMarkdownPath = join(skill.mountPath, "SKILL.md"); - const markerPath = join(skill.mountPath, ".mosoo-skill-cache.json"); - const marker = await readSkillMaterializationMarker(markerPath); +async function renameMaterializationOwner( + owner: MaterializationOwner, + roots: MaterializationRoots, + nextName: string, +): Promise { + const previousName = owner.currentName; + const previousPath = directoryEntryPath(roots.transactionDirectory, previousName); + try { + await assertDirectoryIdentity(owner.ownerDirectory, previousPath, "Skill transaction owner"); + } catch (error) { + await quarantineChangedOwnerEntry(owner, roots, previousName); + throw error; + } + await rename( + directoryEntryPath(roots.transactionDirectory, previousName), + directoryEntryPath(roots.transactionDirectory, nextName), + ); + activeStagingPaths.delete(join(roots.transactionRoot, previousName)); + owner.currentName = nextName; + await roots.transactionDirectory.sync(); + try { + await assertDirectoryIdentity( + owner.ownerDirectory, + directoryEntryPath(roots.transactionDirectory, nextName), + "Skill transaction owner", + ); + } catch (error) { + await quarantineChangedOwnerEntry(owner, roots, nextName); + throw error; + } +} - if ( - marker?.blobSha256 === skill.blobSha256 && - marker.snapshotId === snapshotId && - (await readFile(skillMarkdownPath, "utf8").then( - () => true, - () => false, - )) - ) { - logger.info("driver.skill.materialization.cache_hit", { - skillId: skill.skillId, - skillName: skill.skillName, - snapshotId, - }); +async function quarantineChangedOwnerEntry( + owner: MaterializationOwner, + roots: MaterializationRoots, + entryName: string, +): Promise { + const entryPath = directoryEntryPath(roots.transactionDirectory, entryName); + const stats = await readPathStats(entryPath); + activeStagingPaths.delete(join(roots.transactionRoot, entryName)); + if (stats === null) { + return; + } + if (stats.isSymbolicLink() || !stats.isDirectory()) { + await unlink(entryPath); + await roots.transactionDirectory.sync(); + return; + } + + const quarantineName = `quarantine-${randomUUID()}`; + await rename(entryPath, directoryEntryPath(roots.transactionDirectory, quarantineName)); + owner.currentName = quarantineName; + await roots.transactionDirectory.sync(); +} + +async function retireMaterializationOwner( + owner: MaterializationOwner, + roots: MaterializationRoots, +): Promise { + await renameMaterializationOwner(owner, roots, `quarantine-${randomUUID()}`); +} + +async function openExistingMaterializationOwner( + roots: MaterializationRoots, +): Promise { + const currentPath = directoryEntryPath(roots.transactionDirectory, SKILL_TRANSACTION_ACTIVE_NAME); + const ownerDirectory = await openRealDirectory(currentPath, "Active skill transaction owner"); + let newDirectory: FileHandle | null = null; + try { + newDirectory = await openOptionalRealDirectory( + directoryEntryPath(ownerDirectory, "new"), + "Active new skill owner", + ); return { - mountPath: skill.mountPath, - skillId: skill.skillId, - skillMarkdownPath, - skillName: skill.skillName, - snapshotId, + currentName: SKILL_TRANSACTION_ACTIVE_NAME, + newDirectory, + ownerDirectory, }; + } catch (error) { + const closeFailures = await closeFileHandles([newDirectory, ownerDirectory]); + if (closeFailures.length > 0) { + throw new AggregateError( + [error, ...closeFailures], + "Failed to open the active skill transaction.", + ); + } + throw error; + } +} + +async function hasSkillTransactionCommitMarker(owner: MaterializationOwner): Promise { + let marker: FileHandle; + try { + marker = await open( + directoryEntryPath(owner.ownerDirectory, SKILL_TRANSACTION_COMMIT_MARKER_NAME), + constants.O_RDONLY | constants.O_NOFOLLOW, + ); + } catch (error) { + if (hasErrorCode(error, "ENOENT")) { + return false; + } + throw error; } - await rm(skill.mountPath, { force: true, recursive: true }); - await mkdir(skill.mountPath, { recursive: true }); - const compressed = await downloadSkillPackage(skill); - const actualSha256 = createHash("sha256").update(compressed).digest("hex"); + await using ownedMarker = marker; + const stats = await ownedMarker.stat(); + if (!stats.isFile() || stats.size !== 0) { + throw new Error("Skill transaction commit marker is invalid."); + } + return true; +} - if (actualSha256 !== skill.blobSha256) { - throw new Error(`Skill blob checksum mismatch for ${skill.skillId}.`); +async function disposeMaterializationOwner( + owner: MaterializationOwner, + roots: MaterializationRoots, + logger: Logger, +): Promise { + activeStagingPaths.delete(join(roots.transactionRoot, owner.currentName)); + const failures = await closeFileHandles([owner.newDirectory, owner.ownerDirectory]); + if (owner.currentName === SKILL_TRANSACTION_ACTIVE_NAME) { + return failures; } - const entries = extractZipArchive(compressed, SKILL_ARCHIVE_EXTRACT_OPTIONS); - if (!entries.some((entry) => entry.entryKind === "file" && entry.path === "SKILL.md")) { - throw new Error(`Skill package for ${skill.skillId} does not contain SKILL.md.`); + try { + await withCleanupLock(async () => { + if ( + !(await cleanupQuarantine( + directoryEntryPath(roots.transactionDirectory, owner.currentName), + createQuarantineCleanupBudget(), + roots.transactionDirectory, + )) + ) { + logger.info("driver.skill.materialization.transaction_cleanup_deferred", { + transactionPath: owner.currentName, + }); + } + }); + } catch (error) { + logger.warn("driver.skill.materialization.transaction_cleanup_failed", { + error, + transactionPath: owner.currentName, + }); } - await Promise.all( - entries.map(async (entry) => { - await materializeSkillEntry(skill.mountPath, entry); - }), + return failures; +} + +async function recoverSkillTransactions( + roots: MaterializationRoots, + logger: Logger, + signal: AbortSignal, +): Promise { + const entries = await readDirectoryEntriesBounded( + roots.transactionDirectory, + "Skill transaction root", + MAX_MANAGED_DIRECTORY_ENTRIES, + signal, ); - await writeFile( - markerPath, - JSON.stringify({ - blobSha256: skill.blobSha256, - snapshotId, - }), - "utf8", + const cleanupBudget = createQuarantineCleanupBudget(); + const activeEntry = entries.find((entry) => entry.name === SKILL_TRANSACTION_ACTIVE_NAME); + if (activeEntry !== undefined) { + signal.throwIfAborted(); + if (activeEntry.isSymbolicLink() || !activeEntry.isDirectory()) { + throw new Error("Active skill transaction owner must be a real directory."); + } + const owner = await openExistingMaterializationOwner(roots); + let retiredPath: string | null = null; + let recoveryError: unknown = null; + try { + if (await hasSkillTransactionCommitMarker(owner)) { + await finishCommittedSkillTransaction(owner, roots); + await retireMaterializationOwner(owner, roots); + logger.info("driver.skill.materialization.transaction_finished", {}); + } else { + await rollbackSkillTransaction(owner, roots); + logger.info("driver.skill.materialization.transaction_restored", {}); + } + retiredPath = directoryEntryPath(roots.transactionDirectory, owner.currentName); + } catch (error) { + recoveryError = error; + } + const closeFailures = await closeFileHandles([owner.newDirectory, owner.ownerDirectory]); + if (recoveryError !== null) { + if (closeFailures.length > 0) { + throw new AggregateError( + [recoveryError, ...closeFailures], + "Skill transaction recovery and cleanup failed.", + ); + } + throw recoveryError; + } + if (closeFailures.length > 0) { + throw new AggregateError(closeFailures, "Failed to close recovered skill transaction."); + } + if (retiredPath !== null) { + try { + if (!(await cleanupQuarantine(retiredPath, cleanupBudget, roots.transactionDirectory))) { + logger.info("driver.skill.materialization.transaction_cleanup_deferred", { + transactionPath: basename(retiredPath), + }); + } + } catch (error) { + logger.warn("driver.skill.materialization.transaction_cleanup_failed", { + error, + transactionPath: basename(retiredPath), + }); + } + } + } + + for (const entry of entries) { + signal.throwIfAborted(); + const isStaging = SKILL_STAGING_DIRECTORY_PATTERN.test(entry.name); + const isQuarantine = SKILL_QUARANTINE_DIRECTORY_PATTERN.test(entry.name); + if ( + (!isStaging && !isQuarantine) || + activeStagingPaths.has(join(roots.transactionRoot, entry.name)) + ) { + continue; + } + if (entry.isSymbolicLink() || !entry.isDirectory()) { + await unlink(directoryEntryPath(roots.transactionDirectory, entry.name)); + continue; + } + + try { + if ( + !(await cleanupQuarantine( + directoryEntryPath(roots.transactionDirectory, entry.name), + cleanupBudget, + roots.transactionDirectory, + )) + ) { + logger.info("driver.skill.materialization.transaction_cleanup_deferred", { + transactionPath: entry.name, + }); + } + } catch (error) { + logger.warn("driver.skill.materialization.transaction_cleanup_failed", { + error, + transactionPath: entry.name, + }); + } + } + await roots.transactionDirectory.sync(); +} + +async function openRelativeSkillDirectory( + root: FileHandle, + path: string, + create: boolean, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + let directory = await openRealDirectory( + `${openedDirectoryPath(root)}/.`, + "Staged skill directory", ); - return { - mountPath: skill.mountPath, - skillId: skill.skillId, - skillMarkdownPath, - skillName: skill.skillName, - snapshotId, - }; + try { + for (const segment of path === "." ? [] : path.split("/")) { + signal.throwIfAborted(); + const next = create + ? await ensureRealDirectoryAt(directory, segment, "Staged skill directory", signal) + : await openRealDirectory(directoryEntryPath(directory, segment), "Staged skill directory"); + try { + await directory.close(); + } catch (error) { + await next.close(); + throw error; + } + directory = next; + } + return directory; + } catch (error) { + await directory.close().catch(() => {}); + throw error; + } } -async function materializeSkillEntry(mountPath: string, entry: SkillPackageEntry): Promise { - const absolutePath = join(mountPath, entry.path); +async function materializeSkillEntry( + stagingDirectory: FileHandle, + entry: SkillPackageEntry, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); if (entry.entryKind === "directory") { - await mkdir(absolutePath, { recursive: true }); + await using _directory = await openRelativeSkillDirectory( + stagingDirectory, + entry.path, + true, + signal, + ); return; } - await mkdir(dirname(absolutePath), { recursive: true }); - await writeFile(absolutePath, entry.body); + await using parent = await openRelativeSkillDirectory( + stagingDirectory, + dirname(entry.path), + true, + signal, + ); + signal.throwIfAborted(); + await using file = await open( + directoryEntryPath(parent, basename(entry.path)), + constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, + entry.isExecutable ? 0o755 : 0o644, + ); + await file.writeFile(entry.body, { signal }); + await file.sync(); +} + +async function syncSkillStageDirectories( + stagingDirectory: FileHandle, + entries: readonly SkillPackageEntry[], + signal: AbortSignal, +): Promise { + const directories = new Set(["."]); + + for (const entry of entries) { + let directory = entry.entryKind === "directory" ? entry.path : dirname(entry.path); + while (directory !== ".") { + directories.add(directory); + directory = dirname(directory); + } + } - if (entry.isExecutable) { - await chmod(absolutePath, 0o755); + for (const directory of [...directories].toSorted( + (a, b) => b.split("/").length - a.split("/").length, + )) { + signal.throwIfAborted(); + await using handle = await openRelativeSkillDirectory( + stagingDirectory, + directory, + false, + signal, + ); + await handle.sync(); } } -async function downloadSkillPackage(skill: DriverResolvedSkill): Promise { - const response = await fetch(skill.downloadUrl); +async function downloadSkillPackage( + skill: DriverResolvedSkill, + signal: AbortSignal, +): Promise { + const requestSignal = AbortSignal.any([signal, AbortSignal.timeout(SKILL_DOWNLOAD_TIMEOUT_MS)]); + requestSignal.throwIfAborted(); + const response = await fetch(skill.downloadUrl, { signal: requestSignal }); if (!response.ok) { + await response.body?.cancel().catch(() => {}); throw new Error(`Failed to download skill package for ${skill.skillId}: ${response.status}.`); } + if (response.body === null) { + throw new Error(`Skill package download for ${skill.skillId} has no response body.`); + } - return new Uint8Array(await response.arrayBuffer()); + const contentLength = response.headers.get("content-length"); + if ( + contentLength !== null && + /^\d+$/.test(contentLength) && + Number(contentLength) > MAX_SKILL_COMPRESSED_BYTES + ) { + await response.body.cancel().catch(() => {}); + throw new Error( + `Compressed skill package exceeds the limit (${MAX_SKILL_COMPRESSED_BYTES} bytes).`, + ); + } + + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + const hash = createHash("sha256"); + let totalBytes = 0; + const onAbort = () => { + void reader.cancel(requestSignal.reason).catch(() => {}); + }; + requestSignal.addEventListener("abort", onAbort, { once: true }); + + try { + while (true) { + requestSignal.throwIfAborted(); + const { done, value } = await reader.read(); + requestSignal.throwIfAborted(); + + if (done) { + break; + } + + totalBytes += value.byteLength; + if (totalBytes > MAX_SKILL_COMPRESSED_BYTES) { + const error = new Error( + `Compressed skill package exceeds the limit (${MAX_SKILL_COMPRESSED_BYTES} bytes).`, + ); + await reader.cancel(error).catch(() => {}); + throw error; + } + + chunks.push(value); + hash.update(value); + } + } catch (error) { + await reader.cancel(error).catch(() => {}); + throw error; + } finally { + requestSignal.removeEventListener("abort", onAbort); + reader.releaseLock(); + } + + const bytes = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + + if (hash.digest("hex") !== skill.blobSha256) { + throw new Error(`Skill blob checksum mismatch for ${skill.skillId}.`); + } + return bytes; } diff --git a/src/stores/cma-store.ts b/src/stores/cma-store.ts index 3461508..41de0db 100644 --- a/src/stores/cma-store.ts +++ b/src/stores/cma-store.ts @@ -187,6 +187,10 @@ export interface CmaStore { driverEvent: RuntimeEventEnvelope, ): Promise; archiveEnvironment(id: string): Promise; + /** + * Claims a command only on first admission. An accepted command is never + * reissued because an expired worker cannot prove that its effect did not happen. + */ claimInboundEvent(input: CmaClaimInboundEventInput): Promise; createAgent(input: CmaCreateAgentInput): Promise; createEnvironment(input: CmaCreateEnvironmentInput): Promise; diff --git a/src/stores/memory/cma-memory-store.ts b/src/stores/memory/cma-memory-store.ts index 4d27201..f9957c5 100644 --- a/src/stores/memory/cma-memory-store.ts +++ b/src/stores/memory/cma-memory-store.ts @@ -246,15 +246,6 @@ class CmaMemoryStore implements CmaStore { if (session.status === "terminated") { throw new CmaSessionTerminatedError(input.sessionId); } - - if (existing.lease.expiresAt <= this.#nowIso()) { - existing.lease = this.#createLease(); - return { - claimed: true, - event: structuredClone(existing.record), - lease: structuredClone(existing.lease), - }; - } } return { @@ -332,16 +323,39 @@ class CmaMemoryStore implements CmaStore { throw new CmaStoreConflictError("event", input.commandId); } - const updated = { + let updated = { ...claim.record, commandResult, commandStatus: input.status, cursor: createDriverId(), updatedAt: this.#nowIso(), } satisfies CmaSessionEventRecord; + + let settlementError: unknown; + + try { + this.#eventBroker.assertFrame(updated); + } catch (error) { + if (input.status === "failed") { + throw error; + } + + settlementError = error; + updated = { + ...updated, + commandResult: null, + commandStatus: "failed", + }; + } + this.#eventBroker.replace(input.sessionId, claim.record.id, updated); claim.record = updated; this.#eventBroker.publish(updated); + + if (settlementError !== undefined) { + throw settlementError; + } + return structuredClone(updated); } diff --git a/src/surfaces/cma-http/session-events.ts b/src/surfaces/cma-http/session-events.ts index fedd88b..16df461 100644 --- a/src/surfaces/cma-http/session-events.ts +++ b/src/surfaces/cma-http/session-events.ts @@ -126,7 +126,6 @@ export async function handlePostSessionEvent( try { signal.throwIfAborted(); commandResult = (await dispatchDriverCommand({ command, event, session, signal })) ?? null; - signal.throwIfAborted(); } catch { if (!keeper.signal.aborted) { await store @@ -164,7 +163,7 @@ export async function handlePostSessionEvent( } } -export function createCmaSseResponse(events: AsyncIterable): Response { +function createCmaSseResponse(events: AsyncIterable): Response { let cleanupPromise: Promise | undefined; let iterator: AsyncIterator | undefined; const cleanup = () => diff --git a/src/surfaces/cma-sdk/client.ts b/src/surfaces/cma-sdk/client.ts index e528302..8156757 100644 --- a/src/surfaces/cma-sdk/client.ts +++ b/src/surfaces/cma-sdk/client.ts @@ -1,4 +1,5 @@ import type { CmaInboundEvent } from "../../projections/cma"; +import { CMA_MAX_REPLAY_BYTES } from "../../stores/cma-store"; import type { CmaAgentRecord, CmaCreateAgentInput, @@ -8,6 +9,7 @@ import type { CmaSessionEventRecord, CmaSessionRecord, } from "../../stores/cma-store"; +import { raceWithAbort } from "../../utils/async"; import { CMA_DEFAULT_BETA_HEADER_NAME, CMA_DEFAULT_BETA_HEADER_VALUE } from "../cma-http"; import { decodeCmaSseBytes } from "./sse-bytes-decoder"; import { @@ -15,9 +17,14 @@ import { type CmaSdkClient, type CmaSdkClientOptions, type CmaSdkFetch, + type CmaSdkRequestOptions, + type CmaSdkStreamOptions, type CmaSessionEventDispatchRecord, } from "./types"; +const DEFAULT_TIMEOUT_MS = 30_000; +const MAX_TIMER_MS = 2_147_483_647; + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -60,6 +67,106 @@ function readData(body: unknown): unknown { return body["data"]; } +function assertIntegerInRange( + value: number, + field: string, + minimum: number, + maximum: number, +): void { + if (!Number.isSafeInteger(value) || value < minimum || value > maximum) { + throw new RangeError(`${field} must be an integer between ${minimum} and ${maximum}.`); + } +} + +function responseTooLarge(maxResponseBytes: number): CmaSdkError { + return new CmaSdkError( + 500, + "CMA_SDK_RESPONSE_TOO_LARGE", + `CMA response exceeds ${maxResponseBytes} bytes.`, + null, + ); +} + +async function readResponseBody( + response: Response, + maxResponseBytes: number, + signal: AbortSignal, +): Promise { + if (response.status === 204) { + return null; + } + + const contentLength = Number(response.headers.get("content-length")); + + if (Number.isFinite(contentLength) && contentLength > maxResponseBytes) { + await response.body?.cancel().catch(() => undefined); + throw responseTooLarge(maxResponseBytes); + } + + const reader = response.body?.getReader(); + + if (!reader) { + return null; + } + + let body = new Uint8Array(0); + let cancellation: Promise | undefined; + let completed = false; + let size = 0; + const cancel = () => + (cancellation ??= reader.cancel(signal.reason).then( + () => undefined, + () => undefined, + )); + const onAbort = () => void cancel(); + signal.addEventListener("abort", onAbort, { once: true }); + + try { + signal.throwIfAborted(); + + while (true) { + const chunk = await raceWithAbort(reader.read(), signal); + signal.throwIfAborted(); + + if (chunk.done) { + completed = true; + break; + } + + if (chunk.value.byteLength > maxResponseBytes - size) { + throw responseTooLarge(maxResponseBytes); + } + + const nextSize = size + chunk.value.byteLength; + + if (nextSize > body.byteLength) { + const grown = new Uint8Array( + Math.min(maxResponseBytes, Math.max(nextSize, body.byteLength * 2, 1_024)), + ); + grown.set(body); + body = grown; + } + + body.set(chunk.value, size); + size = nextSize; + } + } finally { + signal.removeEventListener("abort", onAbort); + + if (!completed) { + await cancel(); + } + + reader.releaseLock(); + } + + try { + return JSON.parse(new TextDecoder().decode(body.subarray(0, size))) as unknown; + } catch { + return null; + } +} + export function createCmaSdkClient(options: CmaSdkClientOptions): CmaSdkClient { return new CmaSdkClientCore(options); } @@ -68,11 +175,19 @@ class CmaSdkClientCore implements CmaSdkClient { readonly #baseUrl: URL; readonly #fetch: CmaSdkFetch; readonly #headers: Headers; + readonly #maxResponseBytes: number; + readonly #signal: AbortSignal | undefined; + readonly #timeoutMs: number; constructor(options: CmaSdkClientOptions) { this.#baseUrl = new URL(options.baseUrl); this.#fetch = options.fetch ?? fetch; this.#headers = new Headers(options.headers); + this.#maxResponseBytes = options.maxResponseBytes ?? CMA_MAX_REPLAY_BYTES; + this.#signal = options.signal; + this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; + assertIntegerInRange(this.#maxResponseBytes, "maxResponseBytes", 1, Number.MAX_SAFE_INTEGER); + assertIntegerInRange(this.#timeoutMs, "timeoutMs", 0, MAX_TIMER_MS); if (options.betaHeader !== false) { this.#headers.set( @@ -82,86 +197,165 @@ class CmaSdkClientCore implements CmaSdkClient { } } - async archiveEnvironment(id: string): Promise { + async archiveEnvironment( + id: string, + options?: CmaSdkRequestOptions, + ): Promise { return this.#requestData( `/v1/environments/${encodeURIComponent(id)}/archive`, { method: "POST" }, + options, ); } - async createAgent(input: CmaCreateAgentInput): Promise { - return this.#requestData("/v1/agents", { - body: JSON.stringify(input), - method: "POST", - }); + async createAgent( + input: CmaCreateAgentInput, + options?: CmaSdkRequestOptions, + ): Promise { + return this.#requestData( + "/v1/agents", + { + body: JSON.stringify(input), + method: "POST", + }, + options, + ); } - async createEnvironment(input: CmaCreateEnvironmentInput): Promise { - return this.#requestData("/v1/environments", { - body: JSON.stringify(input), - method: "POST", - }); + async createEnvironment( + input: CmaCreateEnvironmentInput, + options?: CmaSdkRequestOptions, + ): Promise { + return this.#requestData( + "/v1/environments", + { + body: JSON.stringify(input), + method: "POST", + }, + options, + ); } - async createSession(input: CmaCreateSessionInput): Promise { - return this.#requestData("/v1/sessions", { - body: JSON.stringify(input), - method: "POST", - }); + async createSession( + input: CmaCreateSessionInput, + options?: CmaSdkRequestOptions, + ): Promise { + return this.#requestData( + "/v1/sessions", + { + body: JSON.stringify(input), + method: "POST", + }, + options, + ); } - async deleteEnvironment(id: string): Promise { - await this.#request(`/v1/environments/${encodeURIComponent(id)}`, { method: "DELETE" }); + async deleteEnvironment(id: string, options?: CmaSdkRequestOptions): Promise { + await this.#requestBody( + `/v1/environments/${encodeURIComponent(id)}`, + { method: "DELETE" }, + options, + ); } - async getAgent(id: string): Promise { - return this.#requestData(`/v1/agents/${encodeURIComponent(id)}`); + async getAgent(id: string, options?: CmaSdkRequestOptions): Promise { + return this.#requestData(`/v1/agents/${encodeURIComponent(id)}`, {}, options); } - async getEnvironment(id: string): Promise { - return this.#requestData(`/v1/environments/${encodeURIComponent(id)}`); + async getEnvironment(id: string, options?: CmaSdkRequestOptions): Promise { + return this.#requestData( + `/v1/environments/${encodeURIComponent(id)}`, + {}, + options, + ); } - async getSession(id: string): Promise { - return this.#requestData(`/v1/sessions/${encodeURIComponent(id)}`); + async getSession(id: string, options?: CmaSdkRequestOptions): Promise { + return this.#requestData( + `/v1/sessions/${encodeURIComponent(id)}`, + {}, + options, + ); } - async listAgents(): Promise { - return this.#requestData("/v1/agents"); + async listAgents(options?: CmaSdkRequestOptions): Promise { + return this.#requestData("/v1/agents", {}, options); } - async listEnvironments(): Promise { - return this.#requestData("/v1/environments"); + async listEnvironments(options?: CmaSdkRequestOptions): Promise { + return this.#requestData("/v1/environments", {}, options); } - async listSessionEvents(sessionId: string): Promise { + async listSessionEvents( + sessionId: string, + options?: CmaSdkRequestOptions, + ): Promise { return this.#requestData( `/v1/sessions/${encodeURIComponent(sessionId)}/events`, + {}, + options, ); } async sendSessionEvent( sessionId: string, event: CmaInboundEvent, + options?: CmaSdkRequestOptions, ): Promise { return this.#requestData( `/v1/sessions/${encodeURIComponent(sessionId)}/events`, { body: JSON.stringify(event), method: "POST" }, + options, ); } - async *streamSessionEvents( + streamSessionEvents( sessionId: string, - afterCursor?: string, + options: CmaSdkStreamOptions = {}, ): AsyncIterable { - const response = await this.#request(`/v1/sessions/${encodeURIComponent(sessionId)}/events`, { - headers: { - accept: "text/event-stream", - ...(afterCursor === undefined ? {} : { "last-event-id": afterCursor }), + const controller = new AbortController(); + const signal = this.#combineSignals(options.signal, controller.signal); + const iterator = this.#streamSessionEvents(sessionId, options.afterCursor, signal); + const stream: AsyncIterableIterator = { + [Symbol.asyncIterator]() { + return stream; + }, + next() { + return iterator.next(); + }, + return() { + controller.abort(); + return iterator.return(undefined); }, - }); + }; + return stream; + } + + async *#streamSessionEvents( + sessionId: string, + afterCursor: string | undefined, + signal: AbortSignal, + ): AsyncGenerator { + const request = this.#startRequest(signal); + let response: Response; + + try { + response = await this.#request( + `/v1/sessions/${encodeURIComponent(sessionId)}/events`, + { + headers: { + accept: "text/event-stream", + ...(afterCursor === undefined ? {} : { "last-event-id": afterCursor }), + }, + }, + request.signal, + ); + } finally { + request.stop(); + } if (!response.body) { + signal.throwIfAborted(); throw new CmaSdkError( 500, "CMA_SDK_STREAM_UNAVAILABLE", @@ -170,20 +364,25 @@ class CmaSdkClientCore implements CmaSdkClient { ); } - yield* decodeCmaSseBytes(response.body); + yield* decodeCmaSseBytes(response.body, signal); } - async #request(path: string, init: RequestInit = {}): Promise { - const response = await this.#fetch(new URL(path, this.#baseUrl), { - ...init, - headers: this.#createHeaders(init.headers), - }); + async #request(path: string, init: RequestInit, signal: AbortSignal): Promise { + signal.throwIfAborted(); + const response = await raceWithAbort( + this.#fetch(new URL(path, this.#baseUrl), { + ...init, + headers: this.#createHeaders(init.headers, init.body !== null && init.body !== undefined), + signal, + }), + signal, + ); if (response.ok) { return response; } - const body = await this.#readResponseBody(response); + const body = await readResponseBody(response, this.#maxResponseBytes, signal); throw new CmaSdkError( response.status, readErrorCode(body), @@ -192,12 +391,51 @@ class CmaSdkClientCore implements CmaSdkClient { ); } - async #requestData(path: string, init: RequestInit = {}): Promise { - const response = await this.#request(path, init); - return readData(await this.#readResponseBody(response)) as T; + async #requestBody( + path: string, + init: RequestInit, + options: CmaSdkRequestOptions | undefined, + ): Promise { + const request = this.#startRequest(options?.signal); + + try { + const response = await this.#request(path, init, request.signal); + return await readResponseBody(response, this.#maxResponseBytes, request.signal); + } finally { + request.stop(); + } } - #createHeaders(input: HeadersInit | undefined): Headers { + async #requestData( + path: string, + init: RequestInit, + options: CmaSdkRequestOptions | undefined, + ): Promise { + return readData(await this.#requestBody(path, init, options)) as T; + } + + #combineSignals(...signals: readonly (AbortSignal | undefined)[]): AbortSignal { + const defined = [this.#signal, ...signals].filter( + (signal): signal is AbortSignal => signal !== undefined, + ); + return defined.length === 1 ? defined[0]! : AbortSignal.any(defined); + } + + #startRequest(signal?: AbortSignal): { readonly signal: AbortSignal; stop(): void } { + const deadline = new AbortController(); + const timeoutId = setTimeout(() => { + deadline.abort( + new DOMException(`CMA request timed out after ${this.#timeoutMs}ms.`, "TimeoutError"), + ); + }, this.#timeoutMs); + + return { + signal: this.#combineSignals(signal, deadline.signal), + stop: () => clearTimeout(timeoutId), + }; + } + + #createHeaders(input: HeadersInit | undefined, hasBody: boolean): Headers { const headers = new Headers(this.#headers); if (input !== undefined) { @@ -206,22 +444,10 @@ class CmaSdkClientCore implements CmaSdkClient { } } - if (!headers.has("content-type")) { + if (hasBody && !headers.has("content-type")) { headers.set("content-type", "application/json"); } return headers; } - - async #readResponseBody(response: Response): Promise { - if (response.status === 204) { - return null; - } - - try { - return await response.json(); - } catch { - return null; - } - } } diff --git a/src/surfaces/cma-sdk/index.ts b/src/surfaces/cma-sdk/index.ts index 5c5cbb1..d7d232f 100644 --- a/src/surfaces/cma-sdk/index.ts +++ b/src/surfaces/cma-sdk/index.ts @@ -5,5 +5,7 @@ export { type CmaSdkClient, type CmaSdkClientOptions, type CmaSdkFetch, + type CmaSdkRequestOptions, + type CmaSdkStreamOptions, type CmaSessionEventDispatchRecord, } from "./types"; diff --git a/src/surfaces/cma-sdk/sse-bytes-decoder.ts b/src/surfaces/cma-sdk/sse-bytes-decoder.ts index bc3aa48..4a0dd9c 100644 --- a/src/surfaces/cma-sdk/sse-bytes-decoder.ts +++ b/src/surfaces/cma-sdk/sse-bytes-decoder.ts @@ -71,13 +71,22 @@ function sseFrameLimitError(): CmaSdkError { export async function* decodeCmaSseBytes( body: ReadableStream, + signal?: AbortSignal, ): AsyncIterable { const reader = body.getReader(); const decoder = new TextDecoder(); const frame = new Uint8Array(CMA_MAX_EVENT_BYTES + 1); + let cancellation: Promise | undefined; let completed = false; let frameLength = 0; let scanFrom = 0; + const cancel = () => + (cancellation ??= reader.cancel(signal?.reason).then( + () => undefined, + () => undefined, + )); + const onAbort = () => void cancel(); + signal?.addEventListener("abort", onAbort, { once: true }); const consume = (separator: { readonly index: number; @@ -129,8 +138,11 @@ export async function* decodeCmaSseBytes( }; try { + signal?.throwIfAborted(); + while (true) { const chunk = await reader.read(); + signal?.throwIfAborted(); if (chunk.done) { completed = true; @@ -162,8 +174,12 @@ export async function* decodeCmaSseBytes( } } } finally { + signal?.removeEventListener("abort", onAbort); + if (!completed) { - await reader.cancel(); + await cancel(); } + + reader.releaseLock(); } } diff --git a/src/surfaces/cma-sdk/types.ts b/src/surfaces/cma-sdk/types.ts index f47359c..d9ae8d7 100644 --- a/src/surfaces/cma-sdk/types.ts +++ b/src/surfaces/cma-sdk/types.ts @@ -22,6 +22,17 @@ export interface CmaSdkClientOptions { readonly betaHeader?: CmaSdkBetaHeader | false; readonly fetch?: CmaSdkFetch; readonly headers?: HeadersInit; + readonly maxResponseBytes?: number; + readonly signal?: AbortSignal; + readonly timeoutMs?: number; +} + +export interface CmaSdkRequestOptions { + readonly signal?: AbortSignal; +} + +export interface CmaSdkStreamOptions extends CmaSdkRequestOptions { + readonly afterCursor?: string; } export interface CmaSessionEventDispatchRecord { @@ -32,24 +43,34 @@ export interface CmaSessionEventDispatchRecord { } export interface CmaSdkClient { - archiveEnvironment(id: string): Promise; - createAgent(input: CmaCreateAgentInput): Promise; - createEnvironment(input: CmaCreateEnvironmentInput): Promise; - createSession(input: CmaCreateSessionInput): Promise; - deleteEnvironment(id: string): Promise; - getAgent(id: string): Promise; - getEnvironment(id: string): Promise; - getSession(id: string): Promise; - listAgents(): Promise; - listEnvironments(): Promise; - listSessionEvents(sessionId: string): Promise; + archiveEnvironment(id: string, options?: CmaSdkRequestOptions): Promise; + createAgent(input: CmaCreateAgentInput, options?: CmaSdkRequestOptions): Promise; + createEnvironment( + input: CmaCreateEnvironmentInput, + options?: CmaSdkRequestOptions, + ): Promise; + createSession( + input: CmaCreateSessionInput, + options?: CmaSdkRequestOptions, + ): Promise; + deleteEnvironment(id: string, options?: CmaSdkRequestOptions): Promise; + getAgent(id: string, options?: CmaSdkRequestOptions): Promise; + getEnvironment(id: string, options?: CmaSdkRequestOptions): Promise; + getSession(id: string, options?: CmaSdkRequestOptions): Promise; + listAgents(options?: CmaSdkRequestOptions): Promise; + listEnvironments(options?: CmaSdkRequestOptions): Promise; + listSessionEvents( + sessionId: string, + options?: CmaSdkRequestOptions, + ): Promise; sendSessionEvent( sessionId: string, event: CmaInboundEvent, + options?: CmaSdkRequestOptions, ): Promise; streamSessionEvents( sessionId: string, - afterCursor?: string, + options?: CmaSdkStreamOptions, ): AsyncIterable; } diff --git a/tests/acp-agent-process.test.ts b/tests/acp-agent-process.test.ts index 9fc64f2..5916228 100644 --- a/tests/acp-agent-process.test.ts +++ b/tests/acp-agent-process.test.ts @@ -66,9 +66,10 @@ async function startTestAgentProcess( const root = await mkdtemp(join(tmpdir(), "driver-acp-stop-")); const payload = createPayload(root); const readyPath = join(root, "ready"); + let startedChild: AcpAgentProcess | undefined; try { - const child = await startAcpAgentProcess( + const started = await startAcpAgentProcess( harness.context, payload, buildChildEnv(payload), @@ -82,6 +83,9 @@ async function startTestAgentProcess( ...(spawnWatchdog === undefined ? {} : { spawnWatchdog }), }, ); + const child = started.process; + startedChild = child; + await started.ready; while (!(await Bun.file(readyPath).exists())) { await Bun.sleep(5); } @@ -95,6 +99,11 @@ async function startTestAgentProcess( }, }; } catch (error) { + if (startedChild !== undefined) { + await stopAcpAgentProcess(harness.context, startedChild, "test.startup.cleanup").catch( + () => {}, + ); + } await rm(root, { force: true, recursive: true }); throw error; } @@ -139,6 +148,47 @@ async function waitForProcessExit(pid: number): Promise { } describe("ACP agent process lifecycle", () => { + test("does not claim supervision when spawn throws synchronously", async () => { + const harness = createHarness(); + const root = await mkdtemp(join(tmpdir(), "driver-acp-sync-spawn-")); + const payload = createPayload(root); + let child: AcpAgentProcess | undefined; + + try { + await expect( + startAcpAgentProcess( + harness.context, + payload, + buildChildEnv(payload), + new AbortController().signal, + { command: "invalid\0command" }, + ), + ).rejects.toThrow("null bytes"); + + const started = await startAcpAgentProcess( + harness.context, + payload, + buildChildEnv(payload), + new AbortController().signal, + { + args: ["-e", "setInterval(() => {}, 1000)"], + command: process.execPath, + }, + ); + child = started.process; + await started.ready; + await expect( + stopAcpAgentProcess(harness.context, child, "test.stop"), + ).resolves.toBeUndefined(); + } finally { + if (child !== undefined && child.exitCode === null && child.signalCode === null) { + child.kill("SIGKILL"); + } + await rm(root, { force: true, recursive: true }); + await harness.logger.destroy(); + } + }); + test.each(["SIGTERM", "SIGKILL"] as const)("observes exit after %s", async (exitSignal) => { const harness = createHarness(); const process = await startTestAgentProcess( @@ -193,7 +243,7 @@ describe("ACP agent process lifecycle", () => { let child: AcpAgentProcess | undefined; try { - child = await startAcpAgentProcess( + const started = await startAcpAgentProcess( harness.context, payload, buildChildEnv(payload), @@ -207,6 +257,8 @@ describe("ACP agent process lifecycle", () => { spawnWatchdog: fakeWatchdog(cleanup.promise), }, ); + child = started.process; + await started.ready; while (!(await Bun.file(readyPath).exists())) { await Bun.sleep(10); } @@ -274,7 +326,7 @@ describe("ACP agent process lifecycle", () => { }, ); - test("cleans the marked tree when watchdog creation throws and allows a later start", async () => { + test("returns ownership when watchdog creation throws and allows explicit cleanup", async () => { const harness = createHarness(); const root = await mkdtemp(join(tmpdir(), "driver-acp-watchdog-create-")); const payload = createPayload(root); @@ -292,29 +344,33 @@ setInterval(() => {}, 1_000); let rootPid = 0; let shellPid = 0; let workerPid = 0; + let failedChild: AcpAgentProcess | undefined; let retryChild: AcpAgentProcess | undefined; process.env["MOSOO_ACP_FALLBACK_COMMAND"] = process.execPath; process.env["MOSOO_ACP_FALLBACK_ARGS"] = JSON.stringify(["-e", script]); try { - await expect( - startAcpAgentProcess( - harness.context, - payload, - buildChildEnv(payload), - new AbortController().signal, - { - spawnWatchdog: (pid) => { - rootPid = pid; - const deadline = Date.now() + 3_000; - while (!existsSync(workerPidPath) && Date.now() < deadline) { - Atomics.wait(sleeper, 0, 0, 10); - } - throw new Error("test watchdog creation failed"); - }, + const failedStart = await startAcpAgentProcess( + harness.context, + payload, + buildChildEnv(payload), + new AbortController().signal, + { + spawnWatchdog: (pid) => { + rootPid = pid; + const deadline = Date.now() + 3_000; + while (!existsSync(workerPidPath) && Date.now() < deadline) { + Atomics.wait(sleeper, 0, 0, 10); + } + throw new Error("test watchdog creation failed"); }, - ), - ).rejects.toThrow("test watchdog creation failed"); + }, + ); + failedChild = failedStart.process; + await expect(failedStart.ready).rejects.toThrow("test watchdog creation failed"); + await expect( + stopAcpAgentProcess(harness.context, failedChild, "startup.failed"), + ).resolves.toBeUndefined(); [shellPid, workerPid] = await Promise.all([ waitForPidFile(shellPidPath), waitForPidFile(workerPidPath), @@ -325,16 +381,25 @@ setInterval(() => {}, 1_000); "-e", "setInterval(() => {}, 1_000)", ]); - retryChild = await startAcpAgentProcess( + const retryStart = await startAcpAgentProcess( harness.context, payload, buildChildEnv(payload), new AbortController().signal, ); + retryChild = retryStart.process; + await retryStart.ready; await expect( stopAcpAgentProcess(harness.context, retryChild, "test.retry"), ).resolves.toBeUndefined(); } finally { + if ( + failedChild !== undefined && + failedChild.exitCode === null && + failedChild.signalCode === null + ) { + failedChild.kill("SIGKILL"); + } if ( retryChild !== undefined && retryChild.exitCode === null && @@ -362,7 +427,7 @@ setInterval(() => {}, 1_000); } }, 7_000); - test("waits for and propagates rejected cleanup when startup aborts", async () => { + test("keeps the handle when startup aborts so rejected cleanup can be retried", async () => { const harness = createHarness(); const root = await mkdtemp(join(tmpdir(), "driver-acp-startup-abort-")); const payload = createPayload(root); @@ -381,44 +446,37 @@ setInterval(() => {}, 1_000); let rootPid = 0; let shellPid = 0; let workerPid = 0; - - const starting = startAcpAgentProcess( - harness.context, - payload, - buildChildEnv(payload), - controller.signal, - { - args: ["-e", script], - command: process.execPath, - spawnWatchdog: (pid, marker) => { - rootPid = pid; - const deadline = Date.now() + 3_000; - while (!existsSync(workerPidPath) && Date.now() < deadline) { - Atomics.wait(sleeper, 0, 0, 10); - } - controller.abort(); - const watchdog = spawnLinuxProcessTreeWatchdog(pid, marker); - if (watchdog === null) { - throw new Error("Test process supervision could not start."); - } - return { - cleanup: watchdog.cleanup.then(() => cleanup.promise), - process: watchdog.process, - }; - }, - }, - ); - let settled = false; - void starting.then( - () => { - settled = true; - }, - () => { - settled = true; - }, - ); + let child: AcpAgentProcess | undefined; try { + const started = await startAcpAgentProcess( + harness.context, + payload, + buildChildEnv(payload), + controller.signal, + { + args: ["-e", script], + command: process.execPath, + spawnWatchdog: (pid, marker) => { + rootPid = pid; + const deadline = Date.now() + 3_000; + while (!existsSync(workerPidPath) && Date.now() < deadline) { + Atomics.wait(sleeper, 0, 0, 10); + } + controller.abort(); + const watchdog = spawnLinuxProcessTreeWatchdog(pid, marker); + if (watchdog === null) { + throw new Error("Test process supervision could not start."); + } + return { + cleanup: watchdog.cleanup.then(() => cleanup.promise), + process: watchdog.process, + }; + }, + }, + ); + child = started.process; + await expect(started.ready).rejects.toThrow(); [shellPid, workerPid] = await Promise.all([ waitForPidFile(shellPidPath), waitForPidFile(workerPidPath), @@ -426,21 +484,19 @@ setInterval(() => {}, 1_000); await Promise.all( [rootPid, shellPid, workerPid].filter((pid) => pid > 0).map(waitForProcessExit), ); - expect(settled).toBe(false); cleanup.reject(new Error("test startup cleanup rejected")); - const error = await starting.then( - () => null, - (reason: unknown) => reason, + await expect(stopAcpAgentProcess(harness.context, child, "startup.failed")).rejects.toThrow( + "test startup cleanup rejected", ); - expect(error).toBeInstanceOf(AggregateError); - expect( - (error as AggregateError).errors.some( - (entry) => entry instanceof Error && entry.message === "test startup cleanup rejected", - ), - ).toBe(true); + await expect( + stopAcpAgentProcess(harness.context, child, "startup.failed.retry"), + ).resolves.toBeUndefined(); } finally { cleanup.resolve(); + if (child !== undefined && child.exitCode === null && child.signalCode === null) { + child.kill("SIGKILL"); + } for (const pid of [rootPid, shellPid, workerPid]) { if (pid > 0 && isProcessRunning(pid)) { process.kill(pid, "SIGKILL"); @@ -472,13 +528,15 @@ setInterval(() => {}, 1_000); let workerPid = 0; try { - child = await startAcpAgentProcess( + const started = await startAcpAgentProcess( harness.context, payload, buildChildEnv(payload), new AbortController().signal, { args: ["-e", script], command: process.execPath }, ); + child = started.process; + await started.ready; [shellPid, workerPid] = await Promise.all([ waitForPidFile(shellPidPath), waitForPidFile(workerPidPath), diff --git a/tests/acp-client-request-handler.test.ts b/tests/acp-client-request-handler.test.ts index 14eb43f..2fd61b5 100644 --- a/tests/acp-client-request-handler.test.ts +++ b/tests/acp-client-request-handler.test.ts @@ -183,19 +183,25 @@ describe("ACP client request handler", () => { turnEvents: new AcpAssistantTranscriptState(), }); const context = {} as never; - const { terminalId } = await handler.createTerminal(context, { - args: ["-e", "process.stdout.write(process.env.PATH ?? '')"], - command: process.execPath, - env: [], - sessionId: "native-session-1", - }); + await handler.initializePathScope(); + + try { + const { terminalId } = await handler.createTerminal(context, { + args: ["-e", "process.stdout.write(process.env.PATH ?? '')"], + command: process.execPath, + env: [], + sessionId: "native-session-1", + }); - await handler.waitForTerminalExit({ sessionId: "native-session-1", terminalId }); + await handler.waitForTerminalExit({ sessionId: "native-session-1", terminalId }); - expect(handler.terminalOutput({ sessionId: "native-session-1", terminalId }).output).toBe( - "/artifact/bin:/runtime/bin", - ); - await handler.releaseTerminal(context, { sessionId: "native-session-1", terminalId }); + expect(handler.terminalOutput({ sessionId: "native-session-1", terminalId }).output).toBe( + "/artifact/bin:/runtime/bin", + ); + await handler.releaseTerminal(context, { sessionId: "native-session-1", terminalId }); + } finally { + await handler.stopTerminals(context); + } }); test("serializes official SDK notifications and drains scoped suppression", async () => { @@ -317,6 +323,138 @@ describe("ACP client request handler", () => { expect(pushes).toBe(1_025); }); + test("admits replay and completed history but rejects genuinely open retained state", async () => { + const createHandler = () => { + const failures: Error[] = []; + const turnEvents = new AcpAssistantTranscriptState(); + turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never }); + return { + failures, + handler: new AcpClientRequestHandler({ + allowedRoots: [], + cwd: "/workspace", + env: {}, + isCancelling: () => false, + nativeSessionId: () => "native-session-1", + onUpdateFailure: (error) => failures.push(error), + push: async () => {}, + turnEvents, + }), + }; + }; + + const repeated = createHandler(); + const repeatedUpdate = { + sessionId: "native-session-1", + update: { + rawOutput: "x".repeat(4 * 1_024), + sessionUpdate: "tool_call" as const, + status: "in_progress" as const, + title: "tool", + toolCallId: "same-tool", + }, + }; + for (let index = 0; index < 100; index += 1) { + await repeated.handler.enqueueUpdate({} as never, repeatedUpdate); + } + expect(repeated.failures).toEqual([]); + + const completed = createHandler(); + for (let index = 0; index < 600; index += 1) { + await completed.handler.enqueueUpdate({} as never, { + sessionId: "native-session-1", + update: { + sessionUpdate: "tool_call", + status: "completed", + title: "tool", + toolCallId: `tool-${index}`, + }, + }); + } + expect(completed.failures).toEqual([]); + + const open = createHandler(); + for (let index = 0; index < 509; index += 1) { + await open.handler.enqueueUpdate({} as never, { + sessionId: "native-session-1", + update: { + sessionUpdate: "tool_call", + status: "in_progress", + title: "tool", + toolCallId: `open-tool-${index}`, + }, + }); + } + await expect( + open.handler.enqueueUpdate({} as never, { + sessionId: "native-session-1", + update: { + sessionUpdate: "tool_call", + status: "in_progress", + title: "tool", + toolCallId: "open-tool-over-limit", + }, + }), + ).rejects.toThrow("ACP turn state exceeds 510 retained open items"); + expect(open.failures).toHaveLength(1); + + const byteFlood = createHandler(); + const content = "x".repeat(8 * 1_024); + for (let index = 0; index < 47; index += 1) { + await byteFlood.handler.enqueueUpdate({} as never, { + sessionId: "native-session-1", + update: { + content: { text: content, type: "text" }, + messageId: "native-message-1", + sessionUpdate: "agent_message_chunk", + }, + }); + } + await expect( + byteFlood.handler.enqueueUpdate({} as never, { + sessionId: "native-session-1", + update: { + content: { text: content, type: "text" }, + messageId: "native-message-1", + sessionUpdate: "agent_message_chunk", + }, + }), + ).rejects.toThrow("ACP turn state exceeds 393216 retained UTF-8 bytes"); + expect(byteFlood.failures).toHaveLength(1); + }); + + test("routes permission turn-state overflow through provider fatal cleanup", async () => { + const failures: Error[] = []; + const turnEvents = new AcpAssistantTranscriptState(); + turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never }); + const handler = new AcpClientRequestHandler({ + allowedRoots: [], + cwd: "/workspace", + env: {}, + isCancelling: () => false, + nativeSessionId: () => "native-session-1", + onUpdateFailure: (error) => failures.push(error), + push: async () => {}, + turnEvents, + }); + + await expect( + handler.requestPermission( + { ports: { permission: { request: async () => "reject_once" } } } as never, + "r".repeat(400_000), + { + options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], + sessionId: "native-session-1", + toolCall: { + status: "in_progress", + title: "Run command", + } as never, + }, + ), + ).rejects.toThrow("ACP turn state exceeds 393216 retained UTF-8 bytes"); + expect(failures).toHaveLength(1); + }); + test("fails every queued update after the first commit failure", async () => { const failures: Error[] = []; let pushes = 0; @@ -460,7 +598,12 @@ describe("ACP client request handler", () => { const permission = handler.requestPermission(context, 1, { options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], sessionId: "native-session-1", - toolCall: { status: "in_progress", title: "Run command", toolCallId: "tool-1" }, + toolCall: { + rawInput: "x".repeat(250_000), + status: "in_progress", + title: "Run command", + toolCallId: "tool-1", + }, }); void permission.catch(() => {}); @@ -468,7 +611,7 @@ describe("ACP client request handler", () => { const update = handler.enqueueUpdate(context, { sessionId: "native-session-1", update: { - content: { text: "answer", type: "text" }, + content: { text: "a".repeat(250_000), type: "text" }, messageId: "native-message-1", sessionUpdate: "agent_message_chunk", }, @@ -1016,7 +1159,7 @@ describe("ACP client request handler", () => { await expect(handler.drainPermissions()).resolves.toBeUndefined(); }); - test("keeps a late cancelled permission resolution on its originating run", async () => { + test("fences a late cancelled permission resolution after run ownership changes", async () => { const requestedPublishing = Promise.withResolvers(); const releaseRequested = Promise.withResolvers(); let activeRunId = "run-1"; @@ -1095,8 +1238,8 @@ describe("ACP client request handler", () => { releaseRequested.resolve(); await drained; - expect(resolvedRunId).toBe("run-1"); - expect(order).toEqual(["permission.resolved", "run.cancelled"]); + expect(resolvedRunId).toBeNull(); + expect(order).toEqual(["run.cancelled"]); }); test("closes update ingress and drains accepted work before stopping", async () => { diff --git a/tests/acp-configuration.test.ts b/tests/acp-configuration.test.ts index 3aab0ca..b87d5e3 100644 --- a/tests/acp-configuration.test.ts +++ b/tests/acp-configuration.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test"; import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { createBufferedSinkLogger } from "../src/observability"; import { @@ -293,12 +296,25 @@ describe("ACP runtime configuration", () => { }); test("requires host integration snapshot before starting", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-configuration-")); + const payload = { + ...bootPayload, + execution: { + ...bootPayload.execution, + session: { + ...bootPayload.execution.session, + cwd: root, + homePath: join(root, "home"), + sharedRootPath: root, + }, + }, + }; const logger = createBufferedSinkLogger({ level: "debug", service: "acp-configuration-test", sink: async () => {}, }); - const backend = new AcpDriverBackend(bootPayload); + const backend = new AcpDriverBackend(payload); const context = createAgentDriverContext({ eventSink: { commandUpdate: async () => {}, @@ -306,7 +322,7 @@ describe("ACP runtime configuration", () => { pushEvents: async () => ({ accepted: [] }), }, logger, - payload: bootPayload, + payload, permission: { request: async () => "reject_once", }, @@ -318,6 +334,7 @@ describe("ACP runtime configuration", () => { ); } finally { await logger.destroy(); + await rm(root, { force: true, recursive: true }); } }); }); diff --git a/tests/acp-driver-backend.test.ts b/tests/acp-driver-backend.test.ts index e09a950..0dee8a6 100644 --- a/tests/acp-driver-backend.test.ts +++ b/tests/acp-driver-backend.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, spyOn, test } from "bun:test"; import type { ClientContext } from "@agentclientprotocol/sdk"; import { readFileSync } from "node:fs"; import { mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; @@ -18,6 +18,7 @@ import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; import { AcpDriverBackend } from "../src/runtimes/acp/acp-driver-backend"; +import * as acpAgentProcess from "../src/runtimes/acp/acp-agent-process"; import { AcpClientRequestHandler } from "../src/runtimes/acp/acp-client-request-handler"; import { AcpTurnController } from "../src/runtimes/acp/acp-turn-controller"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; @@ -42,6 +43,7 @@ let updateSent = false; let pendingPromptId = null; let pendingProviderCancelPromptId = null; let pendingEndTurnPromptId = null; +let resumeMetadataSent = false; const send = (message) => process.stdout.write(JSON.stringify(message) + "\n"); const requestClient = (message) => { appendFileSync(logPath, message.method + "\n"); @@ -139,10 +141,59 @@ const handle = (message) => { }, 5); return; } + if (process.env.TEST_METADATA_ON_RESUME === "1" && !resumeMetadataSent) { + resumeMetadataSent = true; + for (const update of [ + { + availableCommands: [{ description: "Resume command", name: "resume" }], + sessionUpdate: "available_commands_update", + }, + { + configOptions: [{ currentValue: true, id: "resume-config", type: "boolean" }], + sessionUpdate: "config_option_update", + }, + { + availableModes: [{ id: "resume-mode", name: "Resume mode" }], + currentModeId: "resume-mode", + sessionUpdate: "current_mode_update", + }, + { sessionUpdate: "session_info_update", title: "Resumed session" }, + ]) { + send({ + jsonrpc: "2.0", + method: "session/update", + params: { sessionId: "native-session-1", update }, + }); + } + } sessionReady = true; result = {}; break; case "session/prompt": + if (message.params.prompt[0]?.text === "eof-before-response") { + process.exit(0); + return; + } + if (message.params.prompt[0]?.text === "response-then-eof") { + const update = { + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId: "native-session-1", + update: { + content: { text: "done", type: "text" }, + messageId: "assistant-1", + sessionUpdate: "agent_message_chunk", + }, + }, + }; + const response = { id: message.id, jsonrpc: "2.0", result: { stopReason: "end_turn" } }; + process.stdout.write( + JSON.stringify(update) + "\n" + JSON.stringify(response) + "\n", + () => process.exit(0), + ); + return; + } if (message.params.prompt[0]?.text === "crash") { process.exit(17); } @@ -243,6 +294,26 @@ const handle = (message) => { result = { stopReason: "end_turn" }; break; } + if (message.params.prompt[0]?.text === "invalid-stop-reason") { + for (let index = 0; index < 509; index += 1) { + send({ + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId: "native-session-1", + update: { + kind: "execute", + sessionUpdate: "tool_call", + status: "in_progress", + title: "Tool " + index, + toolCallId: "tool-" + index, + }, + }, + }); + } + result = { stopReason: "s".repeat(1_000) }; + break; + } if (message.params.prompt[0]?.text === "sticky-update-failure") { send({ jsonrpc: "2.0", @@ -377,6 +448,7 @@ async function createHarness( readonly blockResume?: boolean; readonly failResume?: boolean; readonly hangClose?: boolean; + readonly metadataOnResume?: boolean; readonly openCodeInstructions?: boolean; onEvents?(events: readonly DriverEventInput[]): void; readonly permission?: AgentDriverPermissionPort["request"]; @@ -412,6 +484,7 @@ async function createHarness( TEST_BLOCK_RESUME: options.blockResume ? "1" : "0", TEST_FAIL_RESUME: options.failResume ? "1" : "0", TEST_HANG_CLOSE: options.hangClose ? "1" : "0", + TEST_METADATA_ON_RESUME: options.metadataOnResume ? "1" : "0", TEST_SPAWN_LATE_CHILD: options.spawnLateChild ? "1" : "0", TEST_UPDATE_BEFORE_SESSION_RESPONSE: options.updateBeforeSessionResponse ? "1" : "0", ...(options.authenticate ? { MOSOO_ACP_AUTH_METHOD_ID: "test-auth" } : {}), @@ -439,6 +512,7 @@ async function createHarness( }); let acceptedSeq = 0; const lifecycleFailures: Error[] = []; + const lifecycleFailure = Promise.withResolvers(); const publishedEvents: DriverEventInput[] = []; let block: { readonly entered: ReturnType>; @@ -488,7 +562,10 @@ async function createHarness( }, logger, lifecycle: { - fail: (error) => lifecycleFailures.push(error), + fail: (error) => { + lifecycleFailures.push(error); + lifecycleFailure.resolve(error); + }, }, payload, permission: { request: options.permission ?? (async () => "reject_once") }, @@ -541,6 +618,7 @@ async function createHarness( }, events: publishedEvents, latePidPath, + lifecycleFailure: lifecycleFailure.promise, lifecycleFailures, async methods() { return (await readFile(logPath, "utf8")).trim().split("\n").filter(Boolean); @@ -563,6 +641,79 @@ async function createHarness( } describe("ACP driver backend lifecycle", () => { + test("fails before provider setup when a configured root cannot be acquired", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-root-init-failure-")); + const missing = join(root, "missing"); + const boot = { + ...driverBootPayload, + execution: { + ...driverBootPayload.execution, + session: { + ...driverBootPayload.execution.session, + additionalDirectories: [missing], + context: { + ...driverBootPayload.execution.session.context, + homePath: join(root, "home"), + sessionOrganizationPath: root, + }, + cwd: root, + }, + }, + runtime: "acp-fallback", + runtimeTransport: "acp-fallback", + } satisfies DriverBootPayload; + const payload = createDriverStartInputFromBootPayload(boot); + const logger = createBufferedSinkLogger({ + level: "debug", + service: "acp-root-init-failure-test", + sink: async () => {}, + }); + let hostSnapshots = 0; + let materializations = 0; + const context = createAgentDriverContext({ + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, + logger, + payload, + permission: { request: async () => "reject_once" }, + ports: { + hostIntegration: { + snapshot: async () => { + hostSnapshots += 1; + return createDriverHostIntegrationSnapshotFromBootExecution(boot.execution); + }, + }, + skill: { + materialize: async () => { + materializations += 1; + return []; + }, + }, + }, + }); + const backend = new AcpDriverBackend(payload); + + try { + await expect(backend.start(context, new AbortController().signal)).rejects.toMatchObject({ + code: "ENOENT", + }); + expect(hostSnapshots).toBe(0); + expect(materializations).toBe(0); + await expect( + Promise.all([ + backend.stop(context, "retry cleanup", new AbortController().signal), + backend.stop(context, "retry cleanup", new AbortController().signal), + ]), + ).resolves.toEqual([undefined, undefined]); + } finally { + await backend.stop(context, "test cleanup", new AbortController().signal).catch(() => {}); + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + test("uses OpenCode native instructions without a hidden bootstrap prompt", async () => { const harness = await createHarness({ openCodeInstructions: true }); @@ -620,6 +771,89 @@ describe("ACP driver backend lifecycle", () => { } }); + test("commits a complete prompt response before a same-tick transport EOF", async () => { + const originalStop = acpAgentProcess.stopAcpAgentProcess; + let cleanupFailed = false; + const cleanupRejected = Promise.withResolvers(); + let failedProcess: Parameters[1] | null = null; + let retriedOwnedProcess = false; + const stopSpy = spyOn(acpAgentProcess, "stopAcpAgentProcess").mockImplementation( + async (context, process, reason, deadline, signal) => { + if (reason === "connection.failed" && !cleanupFailed) { + cleanupFailed = true; + failedProcess = process; + cleanupRejected.resolve(); + throw new Error("test connection cleanup failed"); + } + if (failedProcess !== null && process === failedProcess) { + retriedOwnedProcess = true; + } + await originalStop(context, process, reason, deadline, signal); + }, + ); + let harness: Awaited> | null = null; + + try { + harness = await createHarness(); + const terminal = harness.blockNext("run.completed"); + const input = harness.backend.handleInput( + harness.context, + { text: "response-then-eof" }, + DRIVER_TEST_IDS.runId as RunId, + ); + void input.catch(() => {}); + + await Promise.all([terminal.entered, cleanupRejected.promise]); + try { + await expect( + harness.backend.handleInput( + harness.context, + { text: "unreachable" }, + DRIVER_TEST_IDS.secondRunId, + ), + ).rejects.toThrow("ACP driver backend connection is not initialized"); + await Promise.resolve(); + expect(harness.lifecycleFailures).toEqual([]); + } finally { + terminal.release(); + } + await expect(input).resolves.toBeUndefined(); + await harness.lifecycleFailure; + + expect(harness.events.filter((event) => event.kind === "run.completed")).toHaveLength(1); + expect(harness.events.some((event) => event.kind === "run.failed")).toBe(false); + expect(harness.lifecycleFailures).toHaveLength(1); + expect(harness.lifecycleFailures[0]).toBeInstanceOf(AggregateError); + await expect( + harness.backend.stop(harness.context, "test retry cleanup", new AbortController().signal), + ).resolves.toBeUndefined(); + expect(retriedOwnedProcess).toBe(true); + } finally { + await harness?.destroy(); + stopSpy.mockRestore(); + } + }); + + test("lets a transport EOF before the prompt response fail the active turn", async () => { + const harness = await createHarness(); + + try { + await expect( + harness.backend.handleInput( + harness.context, + { text: "eof-before-response" }, + DRIVER_TEST_IDS.runId as RunId, + ), + ).rejects.toThrow(); + + expect(harness.events.filter((event) => event.kind === "run.failed")).toHaveLength(1); + expect(harness.events.some((event) => event.kind === "run.completed")).toBe(false); + expect(harness.lifecycleFailures).toEqual([]); + } finally { + await harness.destroy(); + } + }); + test("publishes a thought-only fallback through lossless terminal closures", async () => { const harness = await createHarness(); @@ -726,6 +960,98 @@ describe("ACP driver backend lifecycle", () => { } }); + test("fails a pre-admission cancelled turn when backend stop cleanup fails", async () => { + const originalStop = acpAgentProcess.stopAcpAgentProcess; + let failedProcess: Parameters[1] | null = null; + let retriedOwnedProcess = false; + const stopSpy = spyOn(acpAgentProcess, "stopAcpAgentProcess").mockImplementation( + async (context, process, reason, deadline, signal) => { + if (reason === "test pre-admission stop" && failedProcess === null) { + failedProcess = process; + throw new Error("test pre-admission cleanup failed"); + } + if (failedProcess !== null && process === failedProcess) { + retriedOwnedProcess = true; + } + await originalStop(context, process, reason, deadline, signal); + }, + ); + let harness: Awaited> | null = null; + + try { + harness = await createHarness(); + const gate = harness.blockNext("run.started"); + const input = harness.backend.handleInput( + harness.context, + { text: "must not run" }, + DRIVER_TEST_IDS.runId as RunId, + ); + void input.catch(() => {}); + await gate.entered; + + await expect( + harness.backend.stop( + harness.context, + "test pre-admission stop", + new AbortController().signal, + ), + ).rejects.toThrow("test pre-admission cleanup failed"); + gate.release(); + + await expect(input).rejects.toThrow("cancelled turn process recycle failed"); + expect(await harness.methods()).not.toContain("session/prompt"); + expect( + harness.events + .filter( + (event) => + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed", + ) + .map((event) => event.kind), + ).toEqual(["run.failed"]); + await expect( + harness.backend.stop( + harness.context, + "test pre-admission retry", + new AbortController().signal, + ), + ).resolves.toBeUndefined(); + expect(retriedOwnedProcess).toBe(true); + } finally { + await harness?.destroy(); + stopSpy.mockRestore(); + } + }); + + test("fails a prompt response with an invalid stop reason before terminal projection", async () => { + const harness = await createHarness(); + + try { + await expect( + harness.backend.handleInput( + harness.context, + { text: "invalid-stop-reason" }, + DRIVER_TEST_IDS.runId as RunId, + ), + ).rejects.toThrow("ACP prompt response contains an invalid stop reason"); + + expect( + harness.events + .filter( + (event) => + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed", + ) + .map((event) => event.kind), + ).toEqual(["run.failed"]); + expect(harness.events.filter((event) => event.kind === "item.completed")).toHaveLength(509); + } finally { + await harness.destroy(); + } + }); + test("sends provider cancellation while its observation event is blocked", async () => { const harness = await createHarness(); @@ -877,6 +1203,92 @@ describe("ACP driver backend lifecycle", () => { } }); + test("keeps session metadata admitted while cancelled-turn transcript replay is closed", async () => { + const harness = await createHarness({ metadataOnResume: true }); + + try { + const eventOffset = harness.events.length; + const input = harness.backend.handleInput( + harness.context, + { text: "hang" }, + DRIVER_TEST_IDS.runId as RunId, + ); + void input.catch(() => {}); + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((await harness.methods()).includes("session/prompt")) { + break; + } + await Bun.sleep(5); + } + + await harness.backend.cancelActiveTurn(harness.context, "test cancellation"); + await expect(input).rejects.toThrow("cancelled"); + + expect(harness.events.slice(eventOffset).map((event) => event.kind)).toEqual( + expect.arrayContaining([ + "session.commands.updated", + "session.config.updated", + "session.mode.updated", + "session.info.updated", + "run.cancelled", + ]), + ); + } finally { + await harness.destroy(); + } + }); + + test("publishes a failed terminal even when fatal provider cleanup rejects", async () => { + const harness = await createHarness(); + const promptRequested = Promise.withResolvers(); + const prompt = Promise.withResolvers<{ stopReason: "end_turn" }>(); + const events: DriverEventInput[] = []; + const turn = new AcpTurnController(async (_context, _reason, pushed) => { + events.push(...pushed); + }); + const clientRequests = new AcpClientRequestHandler({ + allowedRoots: [process.cwd()], + cwd: process.cwd(), + env: {}, + isCancelling: () => turn.isCancelling(), + nativeSessionId: () => "native-session-1", + onUpdateFailure: () => {}, + push: async () => {}, + turnEvents: turn.events, + }); + const connection = { + notify: async () => {}, + request: async () => { + promptRequested.resolve(); + return prompt.promise; + }, + } as unknown as ClientContext; + const providerError = new Error("provider transport failed"); + const cleanupError = new Error("provider cleanup failed"); + + try { + const input = turn.handleInput( + harness.context, + { text: "fail" }, + DRIVER_TEST_IDS.runId as RunId, + connection, + "native-session-1", + createDriverHostIntegrationSnapshotFromBootExecution(driverBootPayload.execution), + clientRequests, + ); + void input.catch(() => {}); + await promptRequested.promise; + expect(turn.routeFatal(providerError, Promise.reject(cleanupError))).toBeNull(); + prompt.reject(providerError); + + await expect(input).rejects.toThrow("ACP provider failure cleanup failed"); + expect(events.filter((event) => event.kind === "run.failed")).toHaveLength(1); + expect(events.some((event) => event.kind === "run.cancelled")).toBe(false); + } finally { + await harness.destroy(); + } + }); + test("accepts a notification close after the cancelled terminal is delivered", async () => { const harness = await createHarness(); const promptRequested = Promise.withResolvers(); @@ -1650,6 +2062,63 @@ describe("ACP driver backend lifecycle", () => { } }); + test("fails an active turn after stop cleanup rejects and lets a later stop retry", async () => { + const harness = await createHarness(); + + try { + const input = harness.backend.handleInput( + harness.context, + { text: "hang" }, + DRIVER_TEST_IDS.runId as RunId, + ); + void input.catch(() => {}); + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((await harness.methods()).includes("session/prompt")) { + break; + } + await Bun.sleep(5); + } + + const gate = harness.blockNext("usage.updated"); + const stop = harness.backend.stop( + harness.context, + "test failed stop", + new AbortController().signal, + ); + await gate.entered; + await expect(stop).rejects.toThrow("ACP update drain"); + expect( + harness.events.some( + (event) => + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed", + ), + ).toBe(false); + + const retryStop = harness.backend.stop( + harness.context, + "test retry stop", + new AbortController().signal, + ); + gate.release(); + await expect(input).rejects.toThrow("cancelled turn process recycle failed"); + expect( + harness.events + .filter( + (event) => + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed", + ) + .map((event) => event.kind), + ).toEqual(["run.failed"]); + await expect(retryStop).resolves.toBeUndefined(); + } finally { + await harness.destroy(); + } + }, 10_000); + test("shares one bounded stop budget across a stuck close and update drain", async () => { const harness = await createHarness({ hangClose: true }); diff --git a/tests/acp-event-translator-lifecycle.test.ts b/tests/acp-event-translator-lifecycle.test.ts index 9e1c5a5..69a1ce6 100644 --- a/tests/acp-event-translator-lifecycle.test.ts +++ b/tests/acp-event-translator-lifecycle.test.ts @@ -10,15 +10,18 @@ import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; -import { createDriverId } from "../src/protocol/id"; -import type { EventId, RunId, SessionId } from "../src/protocol/id"; -import { toRuntimeEventInput } from "../src/runtime-events"; +import type { RunId } from "../src/protocol/id"; import { AcpAssistantTranscriptState } from "../src/runtimes/acp/acp-assistant-transcript-state"; import { limitAcpInput } from "../src/runtimes/acp/acp-driver-backend"; import { toPermissionRequest } from "../src/runtimes/acp/acp-permission-events"; import { toPromptStartEvents } from "../src/runtimes/acp/acp-session-events"; +import { + MAX_RUN_TERMINAL_BATCH_BYTES, + MAX_RUN_TERMINAL_BATCH_EVENTS, + preflightDriverEventPush, +} from "../src/runtimes/driver-event-admission"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; -import { CMA_MAX_EVENT_BYTES, encodeCmaSseRecord } from "../src/stores/cma-store"; +import { CMA_MAX_EVENT_BYTES } from "../src/stores/cma-store"; import { createCmaMemoryStore } from "../src/stores/memory"; import { DRIVER_TEST_IDS, @@ -212,6 +215,13 @@ describe("ACP runtime event translation", () => { text: parseText("x".repeat(500_000)), }); const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); + let recordCount = 0; + + expect(events.map((event) => event.kind)).toEqual([ + "message.added", + "run.dispatched", + "run.started", + ]); for (const event of events) { for (const { event: envelope } of toDriverEventEnvelopes( @@ -219,11 +229,12 @@ describe("ACP runtime event translation", () => { event, DRIVER_TEST_IDS.runId, )) { - await expect( - store.appendDriverEvent(DRIVER_TEST_IDS.sessionId, envelope), - ).resolves.toBeArray(); + const records = await store.appendDriverEvent(DRIVER_TEST_IDS.sessionId, envelope); + expect(records).toBeArray(); + recordCount += records.length; } } + expect(recordCount).toBeGreaterThan(0); }); test("normalizes official ACP empty chunks and tool titles before canonical ingress", () => { @@ -380,67 +391,135 @@ describe("ACP runtime event translation", () => { ).toMatchObject({ content: finalText }); }); - test("materializes oversized final text as bounded lossless CMA events", async () => { + test("evicts bounded settled assistant IDs without suppressing the oldest message", () => { const state = new AcpAssistantTranscriptState(); - const runId = createDriverId() as RunId; - const sessionId = createDriverId() as SessionId; - const text = `开始😀${'"\n\\'.repeat(400_000)}结束`; + state.begin({ messageId: "message-1", runId: RUN_ID }); - state.begin({ messageId: createDriverId(), runId }); - const streamed = state.translateUpdate({ + const first = state.translateUpdate({ update: { - content: { text, type: "text" }, - messageId: "native-final", + content: { text: "chunk-0", type: "text" }, + messageId: "native-0", sessionUpdate: "agent_message_chunk", }, }); - const terminal = state.completePrompt("end_turn", null); - const snapshots = terminal.filter( - (event) => event.kind === "message.added" || event.kind === "message.delta", + const firstRuntimeMessageId = eventPayloadString( + requireEvent(first, "message.started"), + "messageId", ); - const store = createCmaMemoryStore({ sessions: [{ id: sessionId }] }); - const records = []; - expect(Buffer.byteLength(JSON.stringify(streamed.at(-1)), "utf8")).toBeGreaterThan( - CMA_MAX_EVENT_BYTES, - ); - expect(snapshots.length).toBeGreaterThan(1); - expect(snapshots.every((event) => event.delivery === "lossless")).toBe(true); - - for (const event of terminal) { - const [envelope] = toRuntimeEventInput( - { - createId: () => createDriverId() as EventId, - occurredAt: "2026-08-13T00:00:00.000Z", - runId, - sessionId, + for (let index = 1; index < 1_026; index += 1) { + state.translateUpdate({ + update: { + content: { text: `chunk-${index}`, type: "text" }, + messageId: `native-${index}`, + sessionUpdate: "agent_message_chunk", }, - event, - ); - records.push(...(await store.appendDriverEvent(sessionId, envelope!))); + }); } + const replayAfterEviction = state.translateUpdate({ + update: { + content: { text: "oldest accepted again", type: "text" }, + messageId: "native-0", + sessionUpdate: "agent_message_chunk", + }, + }); + + expect(eventKinds(replayAfterEviction)).toEqual([ + "message.added", + "message.completed", + "message.started", + "message.delta", + ]); expect( - records.every((record) => encodeCmaSseRecord(record).byteLength < CMA_MAX_EVENT_BYTES), - ).toBe(true); + eventPayloadString(requireEvent(replayAfterEviction, "message.started"), "messageId"), + ).not.toBe(firstRuntimeMessageId); expect( - records - .flatMap(({ event }) => { - if ( - !("sourceEventKind" in event) || - !["message.added", "message.delta"].includes(event.sourceEventKind) || - typeof event.message !== "object" || - event.message === null - ) { - return []; - } + eventPayloadString(requireEvent(replayAfterEviction, "message.delta"), "contentDelta"), + ).toBe("oldest accepted again"); + }); - const message = event.message as Record; - return [String(message["content"] ?? message["contentDelta"] ?? "")]; - }) - .join(""), - ).toBe(text); - expect(requireEvent(terminal, "run.completed").payload).not.toHaveProperty("finalMessageText"); + test("keeps the maximum admitted assistant text inside the shared terminal budget", () => { + const state = new AcpAssistantTranscriptState(); + const content = "x".repeat(8 * 1_024); + + state.begin({ messageId: "message-1", runId: RUN_ID }); + for (let index = 0; index < 47; index += 1) { + state.translateUpdate({ + update: { + content: { text: content, type: "text" }, + messageId: "native-final", + sessionUpdate: "agent_message_chunk", + }, + }); + } + const terminal = state.completePrompt("end_turn", null); + + expect(() => preflightDriverEventPush(terminal, RUN_ID)).not.toThrow(); + expect(Buffer.byteLength(JSON.stringify(terminal), "utf8")).toBeLessThan( + MAX_RUN_TERMINAL_BATCH_BYTES, + ); + }); + + test("keeps the maximum retained item closures inside the shared terminal budget", () => { + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); + + for (let index = 0; index < 509; index += 1) { + state.translateUpdate({ + sessionId: "native-session-1", + update: { + sessionUpdate: "tool_call", + status: "running", + title: "tool", + toolCallId: `tool-${index}-${"x".repeat(642)}`, + }, + }); + } + state.translateUpdate({ + update: { + content: { text: "final", type: "text" }, + messageId: "native-final", + sessionUpdate: "agent_thought_chunk", + }, + }); + const terminal = state.completePrompt("end_turn", { totalTokens: 1 }); + + expect(terminal).toHaveLength(MAX_RUN_TERMINAL_BATCH_EVENTS); + expect(() => preflightDriverEventPush(terminal, RUN_ID)).not.toThrow(); + }); + + test("keeps prompt usage extensions out of the lossless terminal budget", () => { + const state = new AcpAssistantTranscriptState(); + const fallbackToolId = "request-".repeat(40_000); + state.begin({ messageId: "message-1", runId: RUN_ID }); + state.translatePermission({ + params: { + options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], + toolCall: { status: "in_progress" }, + }, + requestId: fallbackToolId, + }); + + const terminal = state.completePrompt("end_turn", { + _meta: { padding: "x".repeat(410_000) }, + inputTokens: 1, + outputTokens: 1, + totalTokens: 2, + }); + const usage = eventPayload(requireEvent(terminal, "usage.updated")); + + expect(usage).toEqual({ + inputTokens: 1, + outputTokens: 1, + source: "prompt_response", + totalTokens: 2, + usageContract: "anthropic_bucketed", + }); + expect(() => preflightDriverEventPush(terminal, RUN_ID)).not.toThrow(); + expect(Buffer.byteLength(JSON.stringify(terminal), "utf8")).toBeLessThan( + MAX_RUN_TERMINAL_BATCH_BYTES, + ); }); test("uses a later identified final after anonymous progress, but fails closed for an anonymous final", () => { @@ -662,13 +741,13 @@ describe("ACP runtime event translation", () => { expect(() => state.translateUpdate({ update: { - rawOutput: "x".repeat(600_000), + rawOutput: "x".repeat(400_000), sessionUpdate: "tool_call_update", - status: "completed", + status: "in_progress", toolCallId: "tool-1", }, }), - ).toThrow("ACP tool update exceeds 524288 UTF-8 bytes"); + ).toThrow("ACP turn state exceeds 393216 retained UTF-8 bytes"); const events = state.translateUpdate({ update: { @@ -695,14 +774,14 @@ describe("ACP runtime event translation", () => { params: { options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], toolCall: { - rawInput: "x".repeat(600_000), + rawInput: "x".repeat(400_000), status: "in_progress", toolCallId: "tool-1", }, }, requestId: "request-1", }), - ).toThrow("ACP tool update exceeds 524288 UTF-8 bytes"); + ).toThrow("ACP turn state exceeds 393216 retained UTF-8 bytes"); expect( state @@ -717,6 +796,33 @@ describe("ACP runtime event translation", () => { ).toEqual(["message.started", "item.started", "tool.call.updated"]); }); + test("accounts for a permission request ID retained as the fallback tool ID", () => { + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); + + expect(() => + state.translatePermission({ + params: { + options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], + toolCall: { status: "in_progress", title: "Run command" }, + }, + requestId: "r".repeat(400_000), + }), + ).toThrow("ACP turn state exceeds 393216 retained UTF-8 bytes"); + + expect( + state + .translatePermission({ + params: { + options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], + toolCall: { status: "in_progress", title: "Run command" }, + }, + requestId: "request-1", + }) + .events.map((event) => event.kind), + ).toEqual(["message.started", "item.started", "tool.call.updated"]); + }); + test("keeps a nonzero execute exit across partial updates", () => { const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", runId: RUN_ID }); @@ -850,6 +956,37 @@ describe("ACP runtime event translation", () => { }, ); + test("evicts completed replay history only after projecting its late update", () => { + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: RUN_ID }); + + for (const toolCallId of ["tool-0", "tool-1"]) { + state.translateUpdate({ + update: { + rawInput: "x".repeat(200_000), + sessionUpdate: "tool_call", + status: "completed", + toolCallId, + }, + }); + } + + const events = state.translateUpdate({ + update: { + rawOutput: "y".repeat(200_000), + sessionUpdate: "tool_call_update", + status: "running", + toolCallId: "tool-0", + }, + }); + + expect(eventKinds(events)).toEqual(["tool.call.updated"]); + expect(eventPayload(events[0]!)).toMatchObject({ + status: "completed", + toolCallId: "tool-0", + }); + }); + test.each([ ["completed", "running", "without content"], ["completed", "running", "with content"], diff --git a/tests/acp-event-translator-session-update.test.ts b/tests/acp-event-translator-session-update.test.ts index 354217b..12fdc8c 100644 --- a/tests/acp-event-translator-session-update.test.ts +++ b/tests/acp-event-translator-session-update.test.ts @@ -464,6 +464,8 @@ describe("ACP runtime event translation", () => { expect(eventPayload(requireEvent(events, "run.failed"))).toMatchObject({ error: { message: "transport closed" }, }); + expect(eventPayload(events[5]!)).not.toHaveProperty("error"); + expect(eventPayload(events[6]!)).not.toHaveProperty("error"); }); test("emits native resume state from ACP session setup", () => { @@ -768,14 +770,6 @@ describe("ACP runtime event translation", () => { cachedWriteTokens: 7, inputTokens: 10, outputTokens: 2, - raw: { - cachedReadTokens: 90, - cachedWriteTokens: 7, - inputTokens: 10, - outputTokens: 2, - thoughtTokens: 3, - totalTokens: 112, - }, source: "prompt_response", thoughtTokens: 3, totalTokens: 112, diff --git a/tests/acp-file-system.test.ts b/tests/acp-file-system.test.ts index 3867b87..c21bf41 100644 --- a/tests/acp-file-system.test.ts +++ b/tests/acp-file-system.test.ts @@ -1,5 +1,21 @@ -import { describe, expect, test } from "bun:test"; -import { mkdtemp, readFile, realpath, rm, symlink, truncate, writeFile } from "node:fs/promises"; +import { afterEach, describe, expect, test } from "bun:test"; +import { truncateSync } from "node:fs"; +import { + chmod, + lstat, + mkdir, + mkdtemp, + readFile, + readlink, + readdir, + realpath, + rename, + rm, + stat, + symlink, + truncate, + writeFile, +} from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -10,15 +26,27 @@ import type { Logger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; import { AcpFileSystem } from "../src/runtimes/acp/acp-file-system"; +import { AcpPathScope } from "../src/runtimes/acp/acp-path-scope"; import { driverStartInput } from "./driver-boot-payload-fixture"; -function createFileSystem(cwd = process.cwd()): AcpFileSystem { - return new AcpFileSystem({ +const pathScopes = new Set(); + +function createFileSystem(cwd = process.cwd(), pathScope?: AcpPathScope): AcpFileSystem { + const scope = pathScope ?? new AcpPathScope({ allowedRoots: [], cwd }); + const fileSystem = new AcpFileSystem({ allowedRoots: [], cwd, + pathScope: scope, }); + pathScopes.add(scope); + return fileSystem; } +afterEach(async () => { + await Promise.all([...pathScopes].map((pathScope) => pathScope.close())); + pathScopes.clear(); +}); + function createContext(events: DriverEventInput[]): { context: AgentDriverContext; logger: Logger; @@ -136,4 +164,227 @@ describe("ACP file system bridge", () => { await rm(root, { force: true, recursive: true }); } }); + + test("reads from the opened capability when an ancestor is exchanged", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-read-race-")); + const outside = await mkdtemp(join(tmpdir(), "driver-acp-fs-read-race-outside-")); + const workspace = join(root, "workspace"); + const retained = join(root, "retained"); + const requestedPath = join(workspace, "note.txt"); + + class ExchangingPathScope extends AcpPathScope { + override async openFile(path: string, label: string) { + const capability = await super.openFile(path, label); + await rename(workspace, retained); + await symlink(outside, workspace); + return capability; + } + } + + try { + await Promise.all([mkdir(workspace), writeFile(join(outside, "note.txt"), "outside")]); + await writeFile(requestedPath, "inside"); + const pathScope = new ExchangingPathScope({ allowedRoots: [], cwd: root }); + const fileSystem = createFileSystem(root, pathScope); + + await expect(fileSystem.readTextFile({ path: requestedPath })).resolves.toEqual({ + content: "inside", + }); + } finally { + await rm(root, { force: true, recursive: true }); + await rm(outside, { force: true, recursive: true }); + } + }); + + test("retains configured root identity after the root path is exchanged", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-root-race-")); + const retained = `${root}-retained`; + const outside = await mkdtemp(join(tmpdir(), "driver-acp-fs-root-race-outside-")); + const requestedRead = join(root, "read.txt"); + const requestedWrite = join(root, "write.txt"); + const events: DriverEventInput[] = []; + const { context, logger } = createContext(events); + const pathScope = new AcpPathScope({ allowedRoots: [], cwd: root }); + const fileSystem = createFileSystem(root, pathScope); + + try { + await Promise.all([ + writeFile(requestedRead, "inside"), + writeFile(join(outside, "read.txt"), "outside"), + writeFile(join(outside, "write.txt"), "outside"), + ]); + await pathScope.initialize(); + await rename(root, retained); + await symlink(outside, root); + + await expect(fileSystem.readTextFile({ path: requestedRead })).resolves.toEqual({ + content: "inside", + }); + await expect( + fileSystem.writeTextFile(context, { content: "written", path: requestedWrite }), + ).resolves.toEqual({}); + expect(await readFile(join(retained, "write.txt"), "utf8")).toBe("written"); + expect(await readFile(join(outside, "write.txt"), "utf8")).toBe("outside"); + expect(events[0]).toMatchObject({ payload: { path: join(retained, "write.txt") } }); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(retained, { force: true, recursive: true }); + await rm(outside, { force: true, recursive: true }); + } + }); + + test("releases partial root acquisition and can initialize again", async () => { + const base = await mkdtemp(join(tmpdir(), "driver-acp-fs-init-retry-")); + const cwd = join(base, "workspace-long"); + const retained = join(base, "retained"); + const missing = join(base, "x"); + await mkdir(cwd); + const pathScope = new AcpPathScope({ allowedRoots: [missing], cwd }); + + try { + await expect(pathScope.initialize()).rejects.toThrow("ENOENT"); + await rename(cwd, retained); + const targets = await Promise.all( + (await readdir(`/proc/${process.pid}/fd`)).map((fd) => + readlink(`/proc/${process.pid}/fd/${fd}`).catch(() => ""), + ), + ); + expect(targets).not.toContain(retained); + + await Promise.all([mkdir(cwd), mkdir(missing)]); + await writeFile(join(cwd, "note.txt"), "replacement"); + await pathScope.initialize(); + const file = await pathScope.openFile(join(cwd, "note.txt"), "test file"); + try { + expect(await file.file.readFile("utf8")).toBe("replacement"); + } finally { + await file.file.close(); + } + } finally { + await pathScope.close(); + await rm(base, { force: true, recursive: true }); + } + }); + + test("enforces the byte limit when a file grows after fstat", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-grow-race-")); + const path = join(root, "growing.txt"); + let checkpoints = 0; + const growthCheckpoint = { + throwIfAborted() { + checkpoints += 1; + if (checkpoints === 3) { + truncateSync(path, 8 * 1_024 * 1_024 + 1); + } + }, + } as unknown as AbortSignal; + + try { + await writeFile(path, "small"); + await expect(createFileSystem(root).readTextFile({ path }, growthCheckpoint)).rejects.toThrow( + "file exceeds 8388608 bytes", + ); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + test("atomically replaces a leaf symlink through the retained parent capability", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-write-race-")); + const outside = await mkdtemp(join(tmpdir(), "driver-acp-fs-write-race-outside-")); + const workspace = join(root, "workspace"); + const retained = join(root, "retained"); + const requestedPath = join(workspace, "note.txt"); + const outsideLeaf = join(outside, "leaf.txt"); + const outsideAncestor = join(outside, "note.txt"); + const events: DriverEventInput[] = []; + const { context, logger } = createContext(events); + + class ExchangingPathScope extends AcpPathScope { + override async openWritable(path: string, label: string) { + const capability = await super.openWritable(path, label); + await rename(workspace, retained); + await symlink(outside, workspace); + return capability; + } + } + + try { + await mkdir(workspace); + await Promise.all([ + writeFile(outsideLeaf, "outside leaf"), + writeFile(outsideAncestor, "outside ancestor"), + ]); + await symlink(outsideLeaf, requestedPath); + const pathScope = new ExchangingPathScope({ allowedRoots: [], cwd: root }); + const fileSystem = createFileSystem(root, pathScope); + + await expect( + fileSystem.writeTextFile(context, { content: "inside", path: requestedPath }), + ).resolves.toEqual({}); + expect(await readFile(join(retained, "note.txt"), "utf8")).toBe("inside"); + expect((await lstat(join(retained, "note.txt"))).isSymbolicLink()).toBe(false); + expect(await readFile(outsideLeaf, "utf8")).toBe("outside leaf"); + expect(await readFile(outsideAncestor, "utf8")).toBe("outside ancestor"); + expect(events[0]).toMatchObject({ payload: { path: join(retained, "note.txt") } }); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(outside, { force: true, recursive: true }); + } + }); + + test("preserves an existing regular file mode during atomic replacement", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-mode-")); + const path = join(root, "script.sh"); + const events: DriverEventInput[] = []; + const { context, logger } = createContext(events); + + try { + await writeFile(path, "old"); + await chmod(path, 0o751); + await createFileSystem(root).writeTextFile(context, { content: "new", path }); + + expect(await readFile(path, "utf8")).toBe("new"); + expect((await stat(path)).mode & 0o777).toBe(0o751); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("removes a partial temporary file when writing is aborted", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-abort-")); + const path = join(root, "note.txt"); + const events: DriverEventInput[] = []; + const { context, logger } = createContext(events); + const aborted = new DOMException("test abort", "AbortError"); + let checkpoints = 0; + const signal = { + throwIfAborted() { + checkpoints += 1; + if (checkpoints === 5) { + throw aborted; + } + }, + } as unknown as AbortSignal; + + try { + await writeFile(path, "before"); + await expect( + createFileSystem(root).writeTextFile( + context, + { content: "x".repeat(512 * 1_024), path }, + signal, + ), + ).rejects.toBe(aborted); + expect(await readFile(path, "utf8")).toBe("before"); + expect((await readdir(root)).filter((entry) => entry.endsWith(".tmp"))).toEqual([]); + expect(events).toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); }); diff --git a/tests/acp-process-supervision.test.ts b/tests/acp-process-supervision.test.ts index d7a1b81..c566c41 100644 --- a/tests/acp-process-supervision.test.ts +++ b/tests/acp-process-supervision.test.ts @@ -81,12 +81,14 @@ const payload = { }; process.env.MOSOO_ACP_FALLBACK_COMMAND = "/bin/sh"; process.env.MOSOO_ACP_FALLBACK_ARGS = JSON.stringify(["-c", ${JSON.stringify(agentSource)}]); -const agent = await startAcpAgentProcess( +const startedAgent = await startAcpAgentProcess( context, payload, { MOSOO_ACP_HOME: ${JSON.stringify(join(directory, "acp-home"))}, PATH: process.env.PATH ?? "" }, new AbortController().signal, ); +const agent = startedAgent.process; +await startedAgent.ready; writeFileSync(${JSON.stringify(paths.agentRoot)}, String(agent.pid)); const terminals = new AcpTerminalManager({ allowedRoots: [], diff --git a/tests/acp-terminal-manager.test.ts b/tests/acp-terminal-manager.test.ts index 45c24f7..4d372d1 100644 --- a/tests/acp-terminal-manager.test.ts +++ b/tests/acp-terminal-manager.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import type { ChildProcess } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rename, rm, symlink } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -9,6 +9,7 @@ import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { createDriverId, isDriverId } from "../src/protocol/id"; import { spawnLinuxProcessTreeWatchdog } from "../src/runtimes/child-process"; +import { AcpPathScope } from "../src/runtimes/acp/acp-path-scope"; import { AcpTerminalManager } from "../src/runtimes/acp/acp-terminal-manager"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { settlePromiseWithTimeout } from "../src/utils/async"; @@ -19,6 +20,8 @@ function createHarness( maxTerminals?: number, recordAfterPush = false, spawnWatchdog?: typeof spawnLinuxProcessTreeWatchdog, + pathScope?: AcpPathScope, + cwd = process.cwd(), ) { const events: DriverEventInput[] = []; const logger = createBufferedSinkLogger({ @@ -37,9 +40,10 @@ function createHarness( }); const manager = new AcpTerminalManager({ allowedRoots: [], - cwd: process.cwd(), + cwd, env: {}, maxTerminals, + pathScope, push: async (_context, _reason, next) => { if (recordAfterPush) { await onPush?.(_reason, next); @@ -94,6 +98,47 @@ async function waitForProcessExit(pid: number): Promise { } describe("ACP terminal manager", () => { + test("binds terminal cwd to the opened directory when its ancestor is exchanged", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-acp-terminal-cwd-race-")); + const outside = await mkdtemp(join(tmpdir(), "mosoo-acp-terminal-cwd-race-outside-")); + const workspace = join(root, "workspace"); + const retained = join(root, "retained"); + + class ExchangingPathScope extends AcpPathScope { + override async openDirectory(path: string, label: string) { + const capability = await super.openDirectory(path, label); + await rename(workspace, retained); + await symlink(outside, workspace); + return capability; + } + } + + await mkdir(workspace); + const pathScope = new ExchangingPathScope({ allowedRoots: [], cwd: root }); + const harness = createHarness(undefined, undefined, false, undefined, pathScope, root); + + try { + const terminal = await harness.manager.create(harness.context, { + args: ["-e", 'require("node:fs").writeFileSync("marker.txt", process.cwd())'], + command: process.execPath, + cwd: workspace, + }); + await harness.manager.waitForExit(terminal); + await harness.manager.release(harness.context, terminal); + + expect(await readFile(join(retained, "marker.txt"), "utf8")).toBe(retained); + expect(existsSync(join(outside, "marker.txt"))).toBe(false); + expect(harness.events.find((event) => event.kind === "terminal.created")).toMatchObject({ + payload: { cwd: retained }, + }); + } finally { + await harness.manager.stopAll(harness.context).catch(() => {}); + await harness.logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(outside, { force: true, recursive: true }); + } + }); + test("retains a valid UTF-8 suffix and exposes exit only after output closes", async () => { const harness = createHarness(); @@ -115,6 +160,7 @@ describe("ACP terminal manager", () => { }); await harness.manager.release(harness.context, { terminalId }); } finally { + await harness.manager.stopAll(harness.context).catch(() => {}); await harness.logger.destroy(); } }); @@ -587,6 +633,52 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } }); + test("rejects a synchronous spawn failure without retaining terminal capacity", async () => { + const harness = createHarness(undefined, 1); + + try { + await expect( + harness.manager.create(harness.context, { + command: "invalid\0command", + }), + ).rejects.toThrow("null bytes"); + const terminal = await harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }); + await harness.manager.waitForExit(terminal); + await harness.manager.release(harness.context, terminal); + } finally { + await harness.manager.stopAll(harness.context); + await harness.logger.destroy(); + } + }); + + test("rejects repeated invalid cwd requests before reserving process ownership", async () => { + const harness = createHarness(undefined, 1); + + try { + for (let attempt = 0; attempt < 64; attempt += 1) { + await expect( + harness.manager.create(harness.context, { + command: process.execPath, + cwd: "/", + }), + ).rejects.toThrow("outside the allowed roots"); + } + + const terminal = await harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }); + await harness.manager.waitForExit(terminal); + await harness.manager.release(harness.context, terminal); + } finally { + await harness.manager.stopAll(harness.context); + await harness.logger.destroy(); + } + }); + test("rejects terminal creation after shutdown starts", async () => { const harness = createHarness(); @@ -818,6 +910,7 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 await expect(operation()).rejects.toThrow("does not exist"); } } finally { + await harness.manager.stopAll(harness.context).catch(() => {}); await harness.logger.destroy(); } }); diff --git a/tests/agent-backend-lifecycle.test.ts b/tests/agent-backend-lifecycle.test.ts new file mode 100644 index 0000000..1b54099 --- /dev/null +++ b/tests/agent-backend-lifecycle.test.ts @@ -0,0 +1,175 @@ +import { describe, expect, test } from "bun:test"; + +import type { AgentDriverContext } from "../src/core/agent-driver-backend"; +import { AgentBackendLifecycle } from "../src/core/agent-backend-lifecycle"; +import { createBackend } from "./driver-runtime-boundary-fixtures"; + +describe("AgentBackendLifecycle", () => { + test("shares a failed stop owner and retries only after it settles", async () => { + const firstStopEntered = Promise.withResolvers(); + const releaseFirstStop = Promise.withResolvers(); + const firstFailure = new Error("first stop failed"); + const backend = createBackend(); + let activeStops = 0; + let maxActiveStops = 0; + let stopCount = 0; + backend.stop = async () => { + stopCount += 1; + activeStops += 1; + maxActiveStops = Math.max(maxActiveStops, activeStops); + + try { + if (stopCount === 1) { + firstStopEntered.resolve(); + await releaseFirstStop.promise; + throw firstFailure; + } + } finally { + activeStops -= 1; + } + }; + const lifecycle = new AgentBackendLifecycle({ + backend, + createContext: () => ({}) as AgentDriverContext, + labels: { + finalStop: "test final stop", + start: "test start", + stop: "test stop", + }, + shutdownSignal: new AbortController().signal, + startTimeoutMs: 1_000, + stopTimeoutMs: 1_000, + }); + + const first = lifecycle.shutdown("first"); + await firstStopEntered.promise; + const concurrent = lifecycle.shutdown("concurrent"); + expect(stopCount).toBe(1); + + releaseFirstStop.resolve(); + const results = await Promise.allSettled([first, concurrent]); + expect(results).toEqual([ + { reason: firstFailure, status: "rejected" }, + { reason: firstFailure, status: "rejected" }, + ]); + + await expect(lifecycle.shutdown("retry")).resolves.toBeUndefined(); + expect(stopCount).toBe(2); + expect(maxActiveStops).toBe(1); + }); + + test("bounds a final stop and serializes its retry behind late cleanup", async () => { + type ManualTimer = { active: boolean; delay: number; run: () => void }; + + const backend = createBackend(); + const startEntered = Promise.withResolvers(); + const releaseStart = Promise.withResolvers(); + const firstStopEntered = Promise.withResolvers(); + const finalStopEntered = Promise.withResolvers(); + const finalStopAborted = Promise.withResolvers(); + const releaseFinalStop = Promise.withResolvers(); + const deferredComplete = Promise.withResolvers(); + let activeStops = 0; + let maxActiveStops = 0; + let stopCount = 0; + backend.start = async () => { + startEntered.resolve(); + await releaseStart.promise; + }; + backend.stop = async (_context, _reason, signal) => { + stopCount += 1; + activeStops += 1; + maxActiveStops = Math.max(maxActiveStops, activeStops); + try { + if (stopCount === 1) { + firstStopEntered.resolve(); + return; + } + if (stopCount === 2) { + finalStopEntered.resolve(); + signal.addEventListener("abort", () => finalStopAborted.resolve(), { once: true }); + await releaseFinalStop.promise; + } + } finally { + activeStops -= 1; + } + }; + const lifecycle = new AgentBackendLifecycle({ + backend, + createContext: () => ({}) as AgentDriverContext, + labels: { + finalStop: "test final stop", + start: "test start", + stop: "test stop", + }, + onDeferredStopComplete: deferredComplete.resolve, + shutdownSignal: new AbortController().signal, + startTimeoutMs: 10_000, + stopTimeoutMs: 1_000, + }); + const start = lifecycle.start(); + await startEntered.promise; + + const nativeClearTimeout = globalThis.clearTimeout; + const nativeNow = Date.now; + const nativeSetTimeout = globalThis.setTimeout; + const timers = new Set(); + let now = 0; + Date.now = () => now; + globalThis.setTimeout = (( + callback: (...args: unknown[]) => void, + delay = 0, + ...args: unknown[] + ) => { + const timer: ManualTimer = { + active: true, + delay, + run: () => { + if (timer.active) { + callback(...args); + } + }, + }; + timers.add(timer); + return timer as unknown as ReturnType; + }) as typeof setTimeout; + globalThis.clearTimeout = ((handle: ReturnType) => { + const timer = handle as unknown as ManualTimer; + if (timers.has(timer)) { + timer.active = false; + } else { + nativeClearTimeout(handle); + } + }) as typeof clearTimeout; + + try { + const shutdown = lifecycle.shutdown("test shutdown"); + await firstStopEntered.promise; + now = 400; + releaseStart.resolve(); + await start; + await finalStopEntered.promise; + + const [finalTimer] = [...timers].filter((timer) => timer.active); + expect(finalTimer?.delay).toBe(600); + finalTimer?.run(); + + await finalStopAborted.promise; + await expect(shutdown).rejects.toThrow("test final stop timed out after 600ms"); + const retry = lifecycle.shutdown("retry"); + await Promise.resolve(); + expect(stopCount).toBe(2); + releaseFinalStop.resolve(); + await expect(retry).resolves.toBeUndefined(); + await deferredComplete.promise; + expect(stopCount).toBe(3); + expect(maxActiveStops).toBe(1); + } finally { + releaseStart.resolve(); + releaseFinalStop.resolve(); + Date.now = nativeNow; + globalThis.clearTimeout = nativeClearTimeout; + globalThis.setTimeout = nativeSetTimeout; + } + }); +}); diff --git a/tests/agent-driver-kernel-async-queue.test.ts b/tests/agent-driver-kernel-async-queue.test.ts index 8c3607c..4349344 100644 --- a/tests/agent-driver-kernel-async-queue.test.ts +++ b/tests/agent-driver-kernel-async-queue.test.ts @@ -265,6 +265,132 @@ describe("AgentDriverKernelCore", () => { }, ); + test("commits only an admitted terminal for the active run", async () => { + const kernel = new AgentDriverKernelCore({ backendFactory: () => createBackend() }); + const events = kernel.events()[Symbol.asyncIterator](); + const delta: DriverEventInput = { + delivery: "lossless", + kind: "message.delta", + payload: { + contentDelta: "x", + messageId: "message-1", + role: "agent", + }, + }; + kernel.beginRun(DRIVER_TEST_IDS.runId); + + for (const runId of [null, DRIVER_TEST_IDS.secondRunId]) { + await expect( + kernel.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + runId, + }, + ], + }), + ).rejects.toThrow("must target the active run"); + } + await kernel.pushEvents({ events: Array.from({ length: 1_024 }, () => delta) }); + await expect( + kernel.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + runId: DRIVER_TEST_IDS.runId, + }, + ], + }), + ).rejects.toThrow("exceeds 1024 items"); + expect(kernel.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + + await kernel.failRun({ + code: "terminal_admission_failed", + details: {}, + message: "Terminal admission failed.", + retryable: false, + }); + await kernel.stop("test.complete"); + + const received: DriverEventInput[] = []; + for await (const event of { [Symbol.asyncIterator]: () => events }) { + received.push(event); + } + + expect(received).toHaveLength(1_025); + expect(received.at(-1)).toMatchObject({ + kind: "run.failed", + runId: DRIVER_TEST_IDS.runId, + }); + }); + + test("linearizes each run at its single final terminal", async () => { + const kernel = new AgentDriverKernelCore({ backendFactory: () => createBackend() }); + const delta: DriverEventInput = { + delivery: "lossless", + kind: "message.delta", + payload: { + contentDelta: "x", + messageId: "message-1", + role: "agent", + }, + }; + const completed: DriverEventInput = { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + runId: DRIVER_TEST_IDS.runId, + }; + kernel.beginRun(DRIVER_TEST_IDS.runId); + + await expect(kernel.pushEvents({ events: [completed, delta] })).rejects.toThrow( + "must be the final event", + ); + await expect( + kernel.pushEvents({ + events: [ + completed, + { + kind: "run.failed", + payload: { error: { code: "failed", message: "failed", retryable: false } }, + runId: DRIVER_TEST_IDS.runId, + }, + ], + }), + ).rejects.toThrow("multiple run terminals"); + await expect( + kernel.pushEvents({ events: [{ ...delta, runId: DRIVER_TEST_IDS.secondRunId }] }), + ).rejects.toThrow("must target the active run"); + await expect( + kernel.pushEvents({ events: [{ ...completed, delivery: "best_effort" }] }), + ).rejects.toThrow("must be lossless"); + expect(kernel.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + + await expect(kernel.pushEvents({ events: [delta, completed] })).resolves.toMatchObject({ + accepted: [{ type: "message.delta" }, { type: "run.completed" }], + }); + expect(kernel.runEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + + await expect(kernel.pushEvents({ events: [delta] })).rejects.toThrow( + "cannot target a terminated run", + ); + await expect(kernel.pushEvents({ events: [{ ...delta, runId: null }] })).resolves.toMatchObject( + { accepted: [{ type: "message.delta" }] }, + ); + + expect(() => kernel.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow( + `Driver run ${DRIVER_TEST_IDS.runId} is already active.`, + ); + expect(kernel.currentRunId()).toBe(DRIVER_TEST_IDS.runId); + expect(kernel.runEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + + kernel.endRun(DRIVER_TEST_IDS.runId); + kernel.beginRun(DRIVER_TEST_IDS.secondRunId); + expect(kernel.currentRunId()).toBe(DRIVER_TEST_IDS.secondRunId); + expect(kernel.runEventTerminal(DRIVER_TEST_IDS.secondRunId)).toBeNull(); + }); + test("treats stop before start as a terminal lifecycle", async () => { const kernel = new AgentDriverKernelCore({ backendFactory: () => createBackend() }); const events = kernel.events()[Symbol.asyncIterator](); diff --git a/tests/agent-driver-kernel-commands.test.ts b/tests/agent-driver-kernel-commands.test.ts index 1e0fbbd..0baacfe 100644 --- a/tests/agent-driver-kernel-commands.test.ts +++ b/tests/agent-driver-kernel-commands.test.ts @@ -57,67 +57,17 @@ describe("AgentDriverKernelCore", () => { expect(events.map((event) => event.kind)).toEqual(["run.started", "run.failed"]); }); - test("automatically retries final cleanup after shutdown times out during startup", async () => { - const backend = createBackend(); - const startEntered = Promise.withResolvers(); - const releaseStart = Promise.withResolvers(); - const finalCleanup = Promise.withResolvers(); - let resourceActive = false; - let stopCount = 0; - let startSignal: AbortSignal | undefined; - backend.start = async (_context, signal) => { - startSignal = signal; - startEntered.resolve(); - await releaseStart.promise; - signal.throwIfAborted(); - resourceActive = true; - }; - backend.stop = async () => { - stopCount += 1; - resourceActive = false; - - if (stopCount === 2) { - finalCleanup.resolve(); - } - }; - const kernel = new AgentDriverKernelCore({ backendFactory: () => backend }); - const nativeSetTimeout = globalThis.setTimeout; - const acceleratedSetTimeout = ( - callback: (...args: unknown[]) => void, - delay?: number, - ...args: unknown[] - ) => nativeSetTimeout(callback, delay === 5_000 ? 10 : delay, ...args); - globalThis.setTimeout = acceleratedSetTimeout as typeof setTimeout; - - try { - const start = kernel.start(bootPayload); - await startEntered.promise; - await expect(kernel.stop("startup stop")).rejects.toThrow("timed out"); - expect(startSignal?.aborted).toBe(true); - expect(startSignal?.reason).toMatchObject({ message: "startup stop" }); - releaseStart.resolve(); - await start; - - const cleaned = await Promise.race([ - finalCleanup.promise.then(() => true), - Bun.sleep(50).then(() => false), - ]); - expect(cleaned).toBe(true); - expect(stopCount).toBe(2); - expect(resourceActive).toBe(false); - } finally { - releaseStart.resolve(); - globalThis.setTimeout = nativeSetTimeout; - } - }); - - test("fails closed for late startup permissions before deferred shutdown closes events", async () => { + test("fails closed for late startup permissions before shutdown closes events", async () => { const backend = createBackend(); const startEntered = Promise.withResolvers(); + const startAborted = Promise.withResolvers(); const releaseStart = Promise.withResolvers(); const latePermissionEntered = Promise.withResolvers(); let latePermission: Promise | null = null; + let startSignal: AbortSignal | undefined; backend.start = async (context, signal) => { + startSignal = signal; + signal.addEventListener("abort", () => startAborted.resolve(), { once: true }); startEntered.resolve(); await releaseStart.promise; latePermission = context.ports.permission.request({ @@ -140,26 +90,25 @@ describe("AgentDriverKernelCore", () => { permissionPolicy: "supervised" as const, }, }; - const nativeSetTimeout = globalThis.setTimeout; - const acceleratedSetTimeout = ( - callback: (...args: unknown[]) => void, - delay?: number, - ...args: unknown[] - ) => nativeSetTimeout(callback, delay === 5_000 ? 10 : delay, ...args); - globalThis.setTimeout = acceleratedSetTimeout as typeof setTimeout; + let start: Promise | null = null; + let stop: Promise | null = null; try { - const start = kernel.start(payload); + start = kernel.start(payload); await startEntered.promise; - await expect(kernel.stop("startup stop")).rejects.toThrow("timed out"); + stop = kernel.stop("startup stop"); + await startAborted.promise; + expect(startSignal?.aborted).toBe(true); + expect(startSignal?.reason).toMatchObject({ message: "startup stop" }); releaseStart.resolve(); await latePermissionEntered.promise; - await start; + await expect(start).resolves.toBeUndefined(); + await expect(stop).resolves.toBeUndefined(); await expect(events.next()).resolves.toEqual({ done: true, value: undefined }); await expect(latePermission).resolves.toBe("reject_once"); } finally { releaseStart.resolve(); - globalThis.setTimeout = nativeSetTimeout; + await Promise.allSettled([start, stop].filter((task) => task !== null)); } }); @@ -307,6 +256,126 @@ describe("AgentDriverKernelCore", () => { await expect(events.next()).resolves.toEqual({ done: true, value: undefined }); }); + test("finalizes events only after a late backend stop and the run task settle", async () => { + type ManualTimer = { active: boolean; run: () => void }; + + const backend = createBackend(); + const inputEntered = Promise.withResolvers(); + const releaseInput = Promise.withResolvers(); + const stopEntered = Promise.withResolvers(); + const stopAborted = Promise.withResolvers(); + const releaseStop = Promise.withResolvers(); + const failure = new Error("backend lifecycle failed"); + let context!: AgentDriverContext; + backend.start = async (startedContext) => { + context = startedContext; + }; + backend.handleInput = async () => { + inputEntered.resolve(); + await releaseInput.promise; + }; + backend.stop = async (_context, _reason, signal) => { + stopEntered.resolve(); + signal.addEventListener("abort", () => stopAborted.resolve(), { once: true }); + await releaseStop.promise; + }; + const kernel = new AgentDriverKernelCore({ backendFactory: () => backend }); + const events = kernel.events()[Symbol.asyncIterator](); + + await kernel.start(bootPayload); + const input = kernel.dispatch({ + commandId: "late-stop-active-input", + input: { text: "wait" }, + kind: "input.start", + requestId: "late-stop-active-request", + runId: DRIVER_TEST_IDS.runId, + }); + void input.catch(() => {}); + await inputEntered.promise; + + const nativeClearTimeout = globalThis.clearTimeout; + const nativeNow = Date.now; + const nativeSetTimeout = globalThis.setTimeout; + let shutdownTimer: ManualTimer | null = null; + Date.now = () => 0; + globalThis.setTimeout = (( + callback: (...args: unknown[]) => void, + delay = 0, + ...args: unknown[] + ) => { + if (delay !== 5_000 || shutdownTimer !== null) { + return nativeSetTimeout(callback, delay, ...args); + } + + const timer: ManualTimer = { + active: true, + run: () => { + if (timer.active) { + callback(...args); + } + }, + }; + shutdownTimer = timer; + return timer as unknown as ReturnType; + }) as typeof setTimeout; + globalThis.clearTimeout = ((handle: ReturnType) => { + if (handle === (shutdownTimer as unknown as ReturnType)) { + if (shutdownTimer !== null) { + shutdownTimer.active = false; + } + } else { + nativeClearTimeout(handle); + } + }) as typeof clearTimeout; + + try { + context.lifecycle.fail(failure); + await stopEntered.promise; + const timer = shutdownTimer as ManualTimer | null; + expect(timer).not.toBeNull(); + timer?.run(); + await stopAborted.promise; + + const nextEvent = events.next(); + const observed = nextEvent.then((value) => ({ kind: "event" as const, value })); + releaseStop.resolve(); + await new Promise((resolve) => setImmediate(resolve)); + await expect( + Promise.race([observed, Promise.resolve({ kind: "pending" as const })]), + ).resolves.toEqual({ kind: "pending" }); + + releaseInput.resolve(); + await expect(input).rejects.toBe(failure); + await expect(observed).resolves.toEqual({ + kind: "event", + value: { + done: false, + value: { + kind: "run.failed", + payload: { + error: { + code: "driver.runtime_failed", + details: {}, + message: failure.message, + retryable: false, + }, + recoverable: false, + }, + runId: DRIVER_TEST_IDS.runId, + }, + }, + }); + await expect(events.next()).resolves.toEqual({ done: true, value: undefined }); + await expect(kernel.stop("join backend failure")).rejects.toBe(failure); + } finally { + releaseInput.resolve(); + releaseStop.resolve(); + Date.now = nativeNow; + globalThis.clearTimeout = nativeClearTimeout; + globalThis.setTimeout = nativeSetTimeout; + } + }); + test("publishes an active backend failure only after input and cleanup settle", async () => { const backend = createBackend(); const cleanupEntered = Promise.withResolvers(); @@ -774,12 +843,14 @@ describe("AgentDriverKernelCore", () => { } }; const kernel = new AgentDriverKernelCore({ backendFactory: () => backend }); + const nativeNow = Date.now; const nativeSetTimeout = globalThis.setTimeout; const acceleratedSetTimeout = ( callback: (...args: unknown[]) => void, delay?: number, ...args: unknown[] ) => nativeSetTimeout(callback, delay === 5_000 ? 10 : delay, ...args); + Date.now = () => 0; globalThis.setTimeout = acceleratedSetTimeout as typeof setTimeout; try { @@ -799,6 +870,7 @@ describe("AgentDriverKernelCore", () => { expect(stopSignals[1]).not.toBe(stopSignals[0]); expect(stopSignals[1]?.aborted).toBe(false); } finally { + Date.now = nativeNow; globalThis.setTimeout = nativeSetTimeout; } }); @@ -807,34 +879,40 @@ describe("AgentDriverKernelCore", () => { const backend = createBackend(); let mcpOutput: string | null = null; let materializedSkillName: string | null = null; - backend.start = async (context: AgentDriverContext) => { - const [skill] = await context.ports.skill.materialize(context.payload.execution); + backend.start = async (context: AgentDriverContext, signal: AbortSignal) => { + const [skill] = await context.ports.skill.materialize(context.payload.execution, signal); materializedSkillName = skill?.skillName ?? null; }; backend.handleInput = async (context: AgentDriverContext) => { - const result = await context.ports.mcp.execute( - { - argumentsJson: '{"ok":true}', - commandId: "mcp-port-1", - kind: "mcp.execute", - requestId: "request-1", - serverId: "server-1", - toolCallId: "tool-1", - toolName: "complete", - }, - new AbortController().signal, - ); + const command = { + argumentsJson: '{"ok":true}', + commandId: "mcp-port-1", + kind: "mcp.execute" as const, + requestId: "request-1", + serverId: "server-1", + toolCallId: "tool-1", + toolName: "complete", + }; + const signal = new AbortController().signal; + await using prepared = await context.ports.mcp.prepare(command, signal); + const result = await prepared.execute({ + effectId: "effect-1", + idempotencyKey: "effect-1", + }); mcpOutput = result.outputText; }; const kernel = new AgentDriverKernelCore({ backendFactory: () => backend, hostPorts: { mcp: { - execute: async (command) => ({ - outputText: `port:${command.toolName}`, - requestId: command.requestId, - serverId: command.serverId, - toolName: command.toolName, + prepare: async (command) => ({ + execute: async () => ({ + outputText: `port:${command.toolName}`, + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }), + async [Symbol.asyncDispose]() {}, }), }, skill: { diff --git a/tests/agent-driver-kernel-lifecycle.test.ts b/tests/agent-driver-kernel-lifecycle.test.ts index b4b5336..d5d7c0a 100644 --- a/tests/agent-driver-kernel-lifecycle.test.ts +++ b/tests/agent-driver-kernel-lifecycle.test.ts @@ -48,17 +48,19 @@ describe("AgentDriverKernelCore", () => { }, hostPorts: { mcp: { - execute: async (command) => { - calls += 1; - const result = { - debug: { nested: "original" }, - outputText: `ran ${command.toolName}`, - requestId: command.requestId, - serverId: command.serverId, - toolName: command.toolName, - }; - return result; - }, + prepare: async (command) => ({ + async execute() { + calls += 1; + return { + debug: { nested: "original" }, + outputText: `ran ${command.toolName}`, + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }; + }, + async [Symbol.asyncDispose]() {}, + }), }, }, }); @@ -310,7 +312,7 @@ describe("AgentDriverKernelCore", () => { }); await nextInputEntered.promise; releaseResolution.resolve(); - await permission; + await expect(permission).resolves.toBe("reject_once"); expect(resolutionRunId).toBe(DRIVER_TEST_IDS.runId); releaseNextInput.resolve(); await expect(nextInput).resolves.toEqual({ requestId: "next-request" }); @@ -483,7 +485,7 @@ describe("AgentDriverKernelCore", () => { }, ); - test("serializes concurrent stop calls with an in-flight start", async () => { + test("joins concurrent stop calls with an in-flight start", async () => { const backend = createBackend(); const startEntered = Promise.withResolvers(); const releaseStart = Promise.withResolvers(); diff --git a/tests/child-process-env.test.ts b/tests/child-process-env.test.ts index 81a593a..bd99a6e 100644 --- a/tests/child-process-env.test.ts +++ b/tests/child-process-env.test.ts @@ -1,13 +1,15 @@ import { describe, expect, test } from "bun:test"; +import { delimiter } from "node:path"; import { parseDriverBootPayload } from "../src/protocol/boot"; import type { DriverExecutionEnvironment } from "../src/protocol/boot"; +import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; import { DRIVER_BOOT_PAYLOAD_ENV_NAME, DRIVER_BOOT_PAYLOAD_FILE_ENV_NAME, buildRuntimeChildProcessEnv, } from "../src/runtimes/child-process-env"; -import { driverBootPayload } from "./driver-boot-payload-fixture"; +import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; function withEnvironmentPaths(paths: unknown) { return { @@ -51,13 +53,39 @@ describe("buildRuntimeChildProcessEnv", () => { }, ); - expect(env["PATH"]).toBe("/artifact/bin:/runtime/bin"); - expect(env["NODE_PATH"]).toBe("/artifact/node:/runtime/node"); - expect(env["PYTHONPATH"]).toBe("/artifact/python:/runtime/python"); + expect(env["PATH"]).toBe(["/artifact/bin", "/runtime/bin"].join(delimiter)); + expect(env["NODE_PATH"]).toBe(["/artifact/node", "/runtime/node"].join(delimiter)); + expect(env["PYTHONPATH"]).toBe(["/artifact/python", "/runtime/python"].join(delimiter)); + }); + + test("inherits and canonicalizes case-insensitive Windows path variables", () => { + const env = buildRuntimeChildProcessEnv( + { executable: ["C:\\artifact"], node: [], python: [] }, + { Path: "C:\\Windows" }, + "win32", + ); + + expect(env["PATH"]).toBe("C:\\artifact;C:\\Windows"); + expect(env).not.toHaveProperty("Path"); }); }); describe("Driver execution environment paths", () => { + test("validates the v2 boot metadata before deriving the internal sandbox identity", () => { + const parsed = parseDriverBootPayload(driverBootPayload); + + expect(parsed.driverControlPort).toBe(20_000); + expect(parsed.driverGeneration).toBe(0); + expect(parsed.heartbeatIntervalMs).toBe(1_000); + expect(createDriverStartInputFromBootPayload(parsed).sandboxId).toBe( + driverBootPayload.execution.session.context.sandboxId, + ); + + expect(() => + parseDriverBootPayload({ ...driverBootPayload, sandboxId: DRIVER_TEST_IDS.secondRunId }), + ).toThrow("sandbox IDs must match"); + }); + test("parses absolute path arrays as an additive protocol version 1 field", () => { const paths = { executable: ["/artifact/bin"], @@ -74,9 +102,32 @@ describe("Driver execution environment paths", () => { expect(parseDriverBootPayload(driverBootPayload).execution.environment.paths).toBeUndefined(); }); - test.each(["relative/path", "/artifact/\0bin"])("rejects invalid path %j", (path) => { - expect(() => - parseDriverBootPayload(withEnvironmentPaths({ executable: [path], node: [], python: [] })), - ).toThrow("execution.environment.paths.executable[0]"); + test.each(["relative/path", "/artifact/\0bin", `/trusted${delimiter}/tmp/untrusted`])( + "rejects invalid path %j", + (path) => { + expect(() => + parseDriverBootPayload(withEnvironmentPaths({ executable: [path], node: [], python: [] })), + ).toThrow("execution.environment.paths.executable[0]"); + }, + ); + + test("preserves dangerous environment variable names as data", () => { + const payload = structuredClone(driverBootPayload); + payload.execution.environment.variables = JSON.parse('{"__proto__":"value"}'); + + const variables = parseDriverBootPayload(payload).execution.environment.variables; + expect(Object.hasOwn(variables, "__proto__")).toBe(true); + expect(variables["__proto__"]).toBe("value"); + }); + + test.each([ + ["", "value"], + ["A=B", "value"], + ["NAME", "bad\0value"], + ])("rejects invalid environment entry %j", (name, value) => { + const payload = structuredClone(driverBootPayload); + payload.execution.environment.variables = { [name]: value }; + + expect(() => parseDriverBootPayload(payload)).toThrow("must be a valid environment entry"); }); }); diff --git a/tests/claude-agent-sdk-driver-backend.test.ts b/tests/claude-agent-sdk-driver-backend.test.ts index b3eb397..0a7edf5 100644 --- a/tests/claude-agent-sdk-driver-backend.test.ts +++ b/tests/claude-agent-sdk-driver-backend.test.ts @@ -317,6 +317,52 @@ describe("Claude Agent SDK driver backend", () => { expect(optionSessionIds).toEqual([null, null, "native-session-1"]); }); + test("retains late prewarm cleanup ownership until stop retries it", async () => { + process.env[PREWARM_ENV] = "1"; + const startup = Promise.withResolvers(); + const startupCalled = Promise.withResolvers(); + const processExit = Promise.withResolvers(); + const warmClosed = Promise.withResolvers(); + let optionCalls = 0; + let cleanupRetries = 0; + const harness = createHarness({ + createQueryOptions: async (input) => { + optionCalls += 1; + if (optionCalls === 1) { + input.processTasks?.add(processExit.promise); + registerClaudeTaskRetry(processExit.promise, async () => { + cleanupRetries += 1; + }); + } + return { abortController: input.abortController }; + }, + query: () => fakeQuery([resultMessage()]), + startup: async () => { + startupCalled.resolve(); + return startup.promise; + }, + }); + + await harness.backend.start(harness.context, new AbortController().signal); + await startupCalled.promise; + await harness.backend.handleInput(harness.context, { text: "first" }, DRIVER_TEST_IDS.runId); + + startup.resolve({ + close: () => warmClosed.resolve(), + query: () => fakeQuery([resultMessage("stale-session")]), + async [Symbol.asyncDispose]() {}, + }); + await warmClosed.promise; + processExit.reject(new Error("late prewarm cleanup failed")); + await nextEventLoopTurn(); + + await expect( + harness.backend.stop(harness.context, "test.stop", new AbortController().signal), + ).resolves.toBeUndefined(); + expect(cleanupRetries).toBe(1); + await harness.logger.destroy(); + }); + test("stop joins a prewarm that ignores cancellation until startup settles", async () => { process.env[PREWARM_ENV] = "1"; const startup = Promise.withResolvers(); @@ -538,6 +584,47 @@ describe("Claude Agent SDK driver backend", () => { await harness.logger.destroy(); }); + test("does not lose a permanent prewarm cleanup failure while retrying its sibling", async () => { + process.env[PREWARM_ENV] = "1"; + const permanentExit = Promise.withResolvers(); + const retryableExit = Promise.withResolvers(); + const startupCalled = Promise.withResolvers(); + const permanentError = new Error("permanent prewarm cleanup failure"); + let cleanupRetries = 0; + const harness = createHarness({ + createQueryOptions: async (input) => { + input.processTasks?.add(permanentExit.promise); + input.processTasks?.add(retryableExit.promise); + registerClaudeTaskRetry(retryableExit.promise, async () => { + cleanupRetries += 1; + }); + return { abortController: input.abortController }; + }, + query: () => fakeQuery([resultMessage()]), + startup: async () => { + startupCalled.resolve(); + throw new Error("prewarm startup failed"); + }, + }); + + await harness.backend.start(harness.context, new AbortController().signal); + await startupCalled.promise; + await nextEventLoopTurn(); + permanentExit.reject(permanentError); + retryableExit.reject(new Error("retryable prewarm cleanup failure")); + await nextEventLoopTurn(); + + await expect( + harness.backend.stop(harness.context, "test.stop", new AbortController().signal), + ).rejects.toBe(permanentError); + expect(cleanupRetries).toBe(1); + await expect( + harness.backend.stop(harness.context, "test.stop.retry", new AbortController().signal), + ).rejects.toBe(permanentError); + expect(cleanupRetries).toBe(1); + await harness.logger.destroy(); + }); + test("concurrent stops share the same prewarm join", async () => { process.env[PREWARM_ENV] = "1"; const startup = Promise.withResolvers(); diff --git a/tests/claude-agent-sdk-durability.test.ts b/tests/claude-agent-sdk-durability.test.ts index 390326e..1cb4e6b 100644 --- a/tests/claude-agent-sdk-durability.test.ts +++ b/tests/claude-agent-sdk-durability.test.ts @@ -692,8 +692,15 @@ describe("Claude Agent SDK durable event boundaries", () => { } as unknown as SDKMessage, runId, ); - await expect(translator.endActiveThought(context)).rejects.toThrow("thought delivery failed"); - await expect(translator.endActiveThought(context)).resolves.toBeUndefined(); + const messageStop = { + event: { type: "message_stop" }, + type: "stream_event", + uuid: "thought-wire", + } as unknown as SDKMessage; + await expect(translator.handleSdkMessage(context, messageStop, runId)).rejects.toThrow( + "thought delivery failed", + ); + await expect(translator.handleSdkMessage(context, messageStop, runId)).resolves.toBe(false); expect(reasons.filter((reason) => reason === "driver.claude.thought.completed")).toHaveLength( 2, ); diff --git a/tests/claude-agent-sdk-process.test.ts b/tests/claude-agent-sdk-process.test.ts index eddc53a..b3b95f4 100644 --- a/tests/claude-agent-sdk-process.test.ts +++ b/tests/claude-agent-sdk-process.test.ts @@ -98,6 +98,27 @@ async function expectExited(pid: number): Promise { } describe.skipIf(process.platform !== "linux")("Claude Agent SDK process supervision", () => { + test("does not claim supervision when spawn throws synchronously", async () => { + const controller = new AbortController(); + const processTasks = new Set>(); + + expect(() => + spawnClaudeCodeProcess( + { + command: "invalid\0command", + args: [], + env: {}, + signal: controller.signal, + }, + () => {}, + controller.signal, + processTasks, + ), + ).toThrow("null bytes"); + expect(processTasks.size).toBe(0); + await expect(drainClaudeTasks(processTasks)).resolves.toBeUndefined(); + }); + test("does not retry cleanup before its original attempt fails", async () => { const cleanupError = new Error("initial cleanup failed"); const initialCleanup = Promise.withResolvers(); diff --git a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts index 3b71422..92e5360 100644 --- a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts @@ -6,7 +6,7 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; -import type { RunId } from "../src/protocol/id"; +import type { MessageId, RunId } from "../src/protocol/id"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { ClaudeAgentSdkMessageTranslator } from "../src/runtimes/claude/agent-sdk-message-translator"; @@ -70,17 +70,17 @@ function readClaudeProviderFixtureCase(path: string): ClaudeProviderFixtureCase }; } -function collectDriverIds(value: unknown, ids: Set): void { +function collectDriverIds(value: unknown, aliases: Map): void { if (typeof value === "string") { - if (isDriverId(value)) { - ids.add(value); + if (isDriverId(value) && !aliases.has(value)) { + aliases.set(value, aliases.size === 0 ? "" : ``); } return; } if (Array.isArray(value)) { for (const entry of value) { - collectDriverIds(entry, ids); + collectDriverIds(entry, aliases); } return; } @@ -90,7 +90,7 @@ function collectDriverIds(value: unknown, ids: Set): void { } for (const entry of Object.values(value)) { - collectDriverIds(entry, ids); + collectDriverIds(entry, aliases); } } @@ -100,17 +100,17 @@ function isIsoTimestamp(value: string): boolean { function normalizeClaudeValue( value: unknown, - driverIds: ReadonlySet, + driverIdAliases: ReadonlyMap, fieldName?: string, ): unknown { if (typeof value === "string") { - for (const driverId of driverIds) { + for (const [driverId, alias] of driverIdAliases) { if (value === driverId) { - return ""; + return alias; } if (value.startsWith(`${driverId}:`)) { - return value.replace(driverId, ""); + return `${alias}${value.slice(driverId.length)}`; } } @@ -122,7 +122,7 @@ function normalizeClaudeValue( } if (Array.isArray(value)) { - return value.map((entry) => normalizeClaudeValue(entry, driverIds)); + return value.map((entry) => normalizeClaudeValue(entry, driverIdAliases)); } if (!isRecord(value)) { @@ -130,20 +130,20 @@ function normalizeClaudeValue( } const entries = Object.entries(value).flatMap(([key, entry]): [string, unknown][] => - entry === undefined ? [] : [[key, normalizeClaudeValue(entry, driverIds, key)]], + entry === undefined ? [] : [[key, normalizeClaudeValue(entry, driverIdAliases, key)]], ); return Object.fromEntries(entries); } function normalizeClaudeEvents(events: readonly DriverEventInput[]): unknown[] { - const driverIds = new Set(); + const driverIdAliases = new Map(); for (const event of events) { - collectDriverIds(event, driverIds); + collectDriverIds(event, driverIdAliases); } - return events.map((event) => normalizeClaudeValue(event, driverIds)); + return events.map((event) => normalizeClaudeValue(event, driverIdAliases)); } function createHarness() { @@ -189,6 +189,41 @@ function createHarness() { } describe("Claude Agent SDK provider fixtures", () => { + test("preserves Driver ID equivalence classes while normalizing fixtures", () => { + const firstMessageId = "01ARZ3NDEKTSV4RRFFQ69G5FAV" as MessageId; + const secondMessageId = "01ARZ3NDEKTSV4RRFFQ69G5FAW" as MessageId; + + expect( + normalizeClaudeEvents([ + { + kind: "message.started", + payload: { messageId: firstMessageId, role: "agent" }, + }, + { + kind: "thought.started", + payload: { channel: "summary", thoughtId: `${firstMessageId}:thought` }, + }, + { + kind: "message.started", + payload: { messageId: secondMessageId, role: "agent" }, + }, + ]), + ).toEqual([ + { + kind: "message.started", + payload: { messageId: "", role: "agent" }, + }, + { + kind: "thought.started", + payload: { channel: "summary", thoughtId: ":thought" }, + }, + { + kind: "message.started", + payload: { messageId: "", role: "agent" }, + }, + ]); + }); + test("keeps chunked stream deltas and the complete assistant snapshot on one message", async () => { // Real SDK wire shape: every envelope (each stream chunk and the complete // assistant replay) carries a distinct uuid; only the API message id inside diff --git a/tests/cma-http-request.test.ts b/tests/cma-http-request.test.ts index d48c761..79f5294 100644 --- a/tests/cma-http-request.test.ts +++ b/tests/cma-http-request.test.ts @@ -206,33 +206,43 @@ describe("CMA HTTP surface", () => { }); test("does not persist an oversized settlement result", async () => { - const store = createCmaMemoryStore({ sessions: [{ id: "session-1" }] }); + let dispatches = 0; + let now = new Date("2026-01-01T00:00:00.000Z"); + const store = createCmaMemoryStore({ + now: () => now, + sessions: [{ id: "session-1" }], + }); const handler = createCmaHttpHandler({ - dispatchDriverCommand: async () => ({ - outputText: "x".repeat(8 * CMA_MAX_EVENT_BYTES), - requestId: "request-1", - serverId: "server-1", - toolName: "tool-1", - }), + dispatchDriverCommand: async () => { + dispatches += 1; + return { + outputText: "x".repeat(8 * CMA_MAX_EVENT_BYTES), + requestId: "request-1", + serverId: "server-1", + toolName: "tool-1", + }; + }, store, }); - const response = await handler( - jsonRequest("/v1/sessions/session-1/events", "POST", { - argumentsJson: "{}", - commandId: "command-1", - requestId: "request-1", - serverId: "server-1", - toolCallId: "tool-call-1", - toolName: "tool-1", - type: "user.custom_tool_result", - }), - ); + const event = { + argumentsJson: "{}", + commandId: "command-1", + requestId: "request-1", + serverId: "server-1", + toolCallId: "tool-call-1", + toolName: "tool-1", + type: "user.custom_tool_result", + }; + const response = await handler(jsonRequest("/v1/sessions/session-1/events", "POST", event)); expect(response.status).toBe(413); expect(await readJson(response)).toMatchObject({ error: { code: "CMA_RESOURCE_LIMIT" } }); - expect(await store.listSessionEvents("session-1")).toMatchObject([ - { commandStatus: "accepted" }, - ]); + expect(await store.listSessionEvents("session-1")).toMatchObject([{ commandStatus: "failed" }]); + + now = new Date(now.getTime() + 31_000); + const retry = await handler(jsonRequest("/v1/sessions/session-1/events", "POST", event)); + expect(retry.status).toBe(502); + expect(dispatches).toBe(1); }); test("creates, lists, retrieves, archives, and deletes environments", async () => { diff --git a/tests/cma-http-sse.test.ts b/tests/cma-http-sse.test.ts index d6f943e..531a076 100644 --- a/tests/cma-http-sse.test.ts +++ b/tests/cma-http-sse.test.ts @@ -129,7 +129,7 @@ describe("CMA HTTP surface", () => { await events.return?.(); }); - test("reclaims an accepted command after its worker lease expires", async () => { + test("does not redispatch an accepted command after its worker lease expires", async () => { let now = new Date(); let dispatches = 0; const store = createCmaMemoryStore({ @@ -154,11 +154,11 @@ describe("CMA HTTP surface", () => { ); expect(response.status).toBe(202); - expect(dispatches).toBe(1); + expect(dispatches).toBe(0); expect(await readJson(response)).toMatchObject({ data: { event: { - commandStatus: "completed", + commandStatus: "accepted", id: abandoned.event.id, }, }, @@ -211,6 +211,38 @@ describe("CMA HTTP surface", () => { expect(dispatches).toBe(1); }); + test("commits a fulfilled dispatch even when the request aborts before settlement", async () => { + const controller = new AbortController(); + let dispatches = 0; + const handler = createCmaHttpHandler({ + dispatchDriverCommand: async () => { + dispatches += 1; + controller.abort(new Error("client disconnected")); + }, + store: createCmaMemoryStore({ sessions: [{ id: "session-1" }] }), + }); + const event = { commandId: "command-1", type: "user.interrupt" }; + const request = new Request("https://driver.test/v1/sessions/session-1/events", { + body: JSON.stringify(event), + headers: { + [CMA_DEFAULT_BETA_HEADER_NAME]: CMA_DEFAULT_BETA_HEADER_VALUE, + "content-type": "application/json", + }, + method: "POST", + signal: controller.signal, + }); + + const response = await handler(request); + expect(response.status).toBe(202); + expect(await readJson(response)).toMatchObject({ + data: { event: { commandStatus: "completed" } }, + }); + + const retry = await handler(jsonRequest("/v1/sessions/session-1/events", "POST", event)); + expect(retry.status).toBe(202); + expect(dispatches).toBe(1); + }); + test("joins concurrent SSE encode-error and cancel cleanup", async () => { const cleanupEntered = Promise.withResolvers(); const releaseCleanup = Promise.withResolvers(); diff --git a/tests/cma-sdk.test.ts b/tests/cma-sdk.test.ts index 9b83781..3e1eca6 100644 --- a/tests/cma-sdk.test.ts +++ b/tests/cma-sdk.test.ts @@ -11,6 +11,7 @@ import { } from "../src/surfaces/cma-http"; import type { CmaSdkError } from "../src/surfaces/cma-sdk"; import { createCmaSdkClient } from "../src/surfaces/cma-sdk"; +import { promiseWithTimeout } from "../src/utils/async"; describe("CMA SDK client", () => { test("sends the default beta header and decodes JSON data responses", async () => { @@ -58,6 +59,78 @@ describe("CMA SDK client", () => { } satisfies Partial); }); + test.each(["client", "request"] as const)( + "propagates %s cancellation through fetch", + async (scope) => { + const controller = new AbortController(); + const entered = Promise.withResolvers(); + const reason = new Error(`${scope} cancelled`); + let fetchSignal: AbortSignal | undefined; + const client = createCmaSdkClient({ + baseUrl: "https://driver.test", + fetch: async (_input, init) => { + fetchSignal = init?.signal ?? undefined; + entered.resolve(); + return new Promise(() => {}); + }, + ...(scope === "client" ? { signal: controller.signal } : {}), + }); + const pending = client.listAgents( + scope === "request" ? { signal: controller.signal } : undefined, + ); + await entered.promise; + + controller.abort(reason); + + await expect(pending).rejects.toBe(reason); + expect(fetchSignal?.aborted).toBe(true); + }, + ); + + test("times out an unresponsive fetch", async () => { + let fetchSignal: AbortSignal | undefined; + const client = createCmaSdkClient({ + baseUrl: "https://driver.test", + fetch: async (_input, init) => { + fetchSignal = init?.signal ?? undefined; + return new Promise(() => {}); + }, + timeoutMs: 0, + }); + + await expect( + promiseWithTimeout(client.listAgents(), { + label: "CMA SDK timeout", + timeoutMs: 100, + }), + ).rejects.toMatchObject({ name: "TimeoutError" }); + expect(fetchSignal?.aborted).toBe(true); + }); + + test("bounds and cancels JSON response bodies", async () => { + let canceled = false; + const client = createCmaSdkClient({ + baseUrl: "https://driver.test", + fetch: async () => + new Response( + new ReadableStream({ + cancel() { + canceled = true; + }, + start(controller) { + controller.enqueue(new TextEncoder().encode('{"data":["too large"]}')); + }, + }), + ), + maxResponseBytes: 8, + }); + + await expect(client.listAgents()).rejects.toMatchObject({ + code: "CMA_SDK_RESPONSE_TOO_LARGE", + }); + expect(canceled).toBe(true); + }); + test("streams server-sent session event replay through fetch", async () => { const sessionId = createDriverId(); const store = createCmaMemoryStore({ @@ -155,7 +228,10 @@ describe("CMA SDK client", () => { ); const resumed = []; - for await (const event of client.streamSessionEvents(sessionId, first?.cursor)) { + for await (const event of client.streamSessionEvents( + sessionId, + first ? { afterCursor: first.cursor } : {}, + )) { resumed.push(event); break; } @@ -353,4 +429,47 @@ describe("CMA SDK client", () => { await expect(stream.next()).rejects.toMatchObject({ code: "CMA_SDK_FRAME_TOO_LARGE" }); expect(canceled).toBe(true); }); + + test("return interrupts a pending SSE read and waits for cleanup", async () => { + const cleanup = Promise.withResolvers(); + const readEntered = Promise.withResolvers(); + let fetchSignal: AbortSignal | undefined; + const client = createCmaSdkClient({ + baseUrl: "https://driver.test", + fetch: async (_input, init) => { + fetchSignal = init?.signal ?? undefined; + return new Response( + new ReadableStream( + { + cancel() { + cleanup.resolve(); + }, + pull() { + readEntered.resolve(); + }, + }, + { highWaterMark: 0 }, + ), + ); + }, + }); + const iterator = client.streamSessionEvents("session-1")[Symbol.asyncIterator](); + const pending = iterator.next(); + await readEntered.promise; + + const returned = iterator.return?.(); + + if (!returned) { + throw new Error("Expected the CMA stream iterator to support return()."); + } + + await expect( + promiseWithTimeout(pending, { label: "pending CMA stream read", timeoutMs: 100 }), + ).rejects.toMatchObject({ name: "AbortError" }); + await expect( + promiseWithTimeout(returned, { label: "CMA stream return", timeoutMs: 100 }), + ).resolves.toMatchObject({ done: true }); + await promiseWithTimeout(cleanup.promise, { label: "CMA stream cleanup", timeoutMs: 100 }); + expect(fetchSignal?.aborted).toBe(true); + }); }); diff --git a/tests/cma-store-claims.test.ts b/tests/cma-store-claims.test.ts index ecae9e7..a427399 100644 --- a/tests/cma-store-claims.test.ts +++ b/tests/cma-store-claims.test.ts @@ -106,7 +106,7 @@ describe("CMA memory store lifecycle", () => { ).rejects.toBeInstanceOf(CmaStoreConflictError); }); - test("reclaims expired command leases and rejects stale settlers", async () => { + test("never reclaims an expired command with an ambiguous effect", async () => { let now = new Date("2026-01-01T00:00:00.000Z"); const sessionId = createDriverId(); const store = createCmaMemoryStore({ @@ -121,14 +121,12 @@ describe("CMA memory store lifecycle", () => { } now = new Date("2026-01-01T00:00:31.000Z"); - const reclaimed = await store.claimInboundEvent(input); + const retry = await store.claimInboundEvent(input); - if (!reclaimed.claimed) { - throw new Error("Expected the expired command claim to be reclaimed."); - } - - expect(reclaimed.event.id).toBe(first.event.id); - expect(reclaimed.lease.id).not.toBe(first.lease.id); + expect(retry).toMatchObject({ + claimed: false, + event: { commandStatus: "accepted", id: first.event.id }, + }); await expect( store.settleInboundEvent({ commandId: "command-1", @@ -138,15 +136,6 @@ describe("CMA memory store lifecycle", () => { status: "completed", }), ).rejects.toBeInstanceOf(CmaStoreConflictError); - await expect( - store.settleInboundEvent({ - commandId: "command-1", - commandResult: null, - leaseId: reclaimed.lease.id, - sessionId, - status: "completed", - }), - ).resolves.toMatchObject({ commandStatus: "completed" }); }); test.each([ @@ -182,7 +171,7 @@ describe("CMA memory store lifecycle", () => { }); await expect(attempt).rejects.toBeInstanceOf(CmaStoreConflictError); - await expect(store.claimInboundEvent(input)).resolves.toMatchObject({ claimed: true }); + await expect(store.claimInboundEvent(input)).resolves.toMatchObject({ claimed: false }); }); test("keeps a completed settlement idempotent after its lease expires", async () => { diff --git a/tests/cma-store-events.test.ts b/tests/cma-store-events.test.ts index f67301d..f2ba440 100644 --- a/tests/cma-store-events.test.ts +++ b/tests/cma-store-events.test.ts @@ -44,6 +44,49 @@ function driverEvent( } describe("CMA memory store lifecycle", () => { + test("preserves dangerous payload keys as data without changing failure semantics", async () => { + const sessionId = createDriverId(); + const runId = createDriverId(); + const sourceEventId = createDriverId(); + const store = createCmaMemoryStore({ sessions: [{ id: sessionId }] }); + const payload = JSON.parse( + '{"__proto__":{"recoverable":true},"error":{"code":"fatal","details":{},"message":"failed","retryable":false}}', + ); + const event = driverEvent(sessionId, "run.failed", payload, { runId, sourceEventId }); + const eventPayload = event.payload as Record; + + expect(Object.hasOwn(eventPayload, "__proto__")).toBe(true); + expect(eventPayload["recoverable"]).toBeUndefined(); + const first = await store.appendDriverEvent(sessionId, event); + + expect(first).toMatchObject([ + { event: { sessionStatus: "terminated", type: "session.error" } }, + ]); + await expect(store.appendDriverEvent(sessionId, event)).resolves.toEqual(first); + expect(await store.getSession(sessionId)).toMatchObject({ status: "terminated" }); + }); + + test.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY])( + "rejects non-JSON error detail number %s", + (value) => { + expect(() => + driverEvent( + createDriverId(), + "run.failed", + { + error: { + code: "fatal", + details: { value }, + message: "failed", + retryable: false, + }, + }, + { runId: createDriverId() }, + ), + ).toThrow("must be JSON-serializable"); + }, + ); + test("deduplicates stable source events and rejects changed or cross-session identities", async () => { const sessionId = createDriverId(); const otherSessionId = createDriverId(); @@ -251,10 +294,12 @@ describe("CMA memory store lifecycle", () => { expect(encodeCmaSseRecord(record)).toHaveLength(CMA_MAX_EVENT_BYTES); } else { await expect(result).rejects.toThrow("SSE event frame exceeds"); - expect(await store.listSessionEvents(sessionId)).toEqual([claim.event]); + expect(await store.listSessionEvents(sessionId)).toMatchObject([ + { commandResult: null, commandStatus: "failed" }, + ]); await expect(store.claimInboundEvent(input)).resolves.toMatchObject({ claimed: false, - event: { commandStatus: "accepted" }, + event: { commandStatus: "failed" }, }); } }, diff --git a/tests/driver-artifact-contract.test.ts b/tests/driver-artifact-contract.test.ts index 8487b6a..7b53f17 100644 --- a/tests/driver-artifact-contract.test.ts +++ b/tests/driver-artifact-contract.test.ts @@ -221,10 +221,8 @@ describe("driver artifact contract", () => { ); }); - test("runs every live suite through the packed driver controller", () => { + test("routes every live suite through the packed driver artifact", () => { const packageJson = readDriverPackageJson(); - const controller = readText("./driver-artifact-test-controller.ts"); - const liveTest = readText("./driver-artifact-live.test.ts"); const artifactScript = packageJson.scripts?.["test:live:artifact"] ?? ""; expect(artifactScript).toContain("AGENT_DRIVER_LIVE=1"); @@ -237,48 +235,6 @@ describe("driver artifact contract", () => { expect(script).toContain(`AGENT_DRIVER_LIVE_SUITE=${suite}`); expect(script).toContain("tests/driver-artifact-live.test.ts"); } - expect(controller).toContain("export class DriverArtifactTestController"); - expect(controller).toContain("parseDriverEventBatchInput"); - expect(controller).toContain("crashDriver(): void"); - expect(controller).toMatch( - /crashDriver\(\): void \{\s+this\.#signalDriver\("SIGKILL", false\);\s+\}/, - ); - expect(controller).toContain("disconnectDriver(): void"); - expect(controller).toContain("failHeartbeats(): void"); - expect(controller).toContain("signalDriver(signal: NodeJS.Signals): void"); - const scenarioNames = (start: string, end: string) => - Array.from( - liveTest.slice(liveTest.indexOf(start), liveTest.indexOf(end)).matchAll(/\["([^"]+)"/g), - (match) => match[1], - ); - expect(liveTest).toContain("const compatibilityScenarios"); - expect(liveTest).toContain("const lifecycleScenarios"); - expect(liveTest).toContain("const controlScenarios"); - expect(scenarioNames("const compatibilityScenarios", "const lifecycleScenarios")).toEqual([ - "sequential turns", - ]); - expect(scenarioNames("const lifecycleScenarios", "const controlScenarios")).toEqual([ - "workspace Unicode CRUD", - "nonzero command recovery", - "native MCP configuration and tool call", - "native process resume", - "provider crash and native resume", - "stale native resume", - "run.started ACK-boundary, active, replayed, and idle cancellation", - "supervised permission cancellation, rejection, and approval", - "active stop and restart", - ]); - expect(scenarioNames("const controlScenarios", 'describe("packed driver live matrix"')).toEqual( - [ - "process crash and native resume", - "SIGTERM and restart", - "active control disconnect", - "active heartbeat failure", - ], - ); - expect(liveTest).toContain("for (const runtimeCase of runtimeCases)"); - expect(liveTest).toContain("for (const runtimeCase of lifecycleCases)"); - expect(liveTest).not.toMatch(/from ["']\.\.\/src\/(?:core|runtimes)/); }); test("pins runtime executables and keeps release publication monotonic", () => { diff --git a/tests/driver-artifact-live.test.ts b/tests/driver-artifact-live.test.ts index b4e69ff..98ee13e 100644 --- a/tests/driver-artifact-live.test.ts +++ b/tests/driver-artifact-live.test.ts @@ -833,6 +833,7 @@ function createBootPayload(input: { readonly sessionId: string; }): DriverArtifactBootPayload { const runtimeCase = input.runtimeCase; + const sandboxId = createTestId(); return { bootToken: `artifact-test-${input.driverInstanceId}`, @@ -876,7 +877,7 @@ function createBootPayload(input: { executionOwnerUserId: createTestId(), type: "agent", }, - sandboxId: createTestId(), + sandboxId, sandboxKind: "cattle", sandboxSessionId: createTestId(), sandboxSubjectId: input.sessionId, @@ -895,7 +896,7 @@ function createBootPayload(input: { protocolVersion: 2, runtime: runtimeCase.runtime, runtimeTransport: runtimeCase.transport, - sandboxId: createTestId(), + sandboxId, traceparent: "00-00000000000000000000000000000001-0000000000000001-01", }; } diff --git a/tests/driver-event-publisher-admission.test.ts b/tests/driver-event-publisher-admission.test.ts index e708704..ffc336e 100644 --- a/tests/driver-event-publisher-admission.test.ts +++ b/tests/driver-event-publisher-admission.test.ts @@ -389,11 +389,10 @@ describe("DriverEventPublisher", () => { const first = publisher.push(context, "first", [createDelta("a")]); await firstSendEntered.promise; - const blocked = await Promise.race([first.then(() => false), Bun.sleep(10).then(() => true)]); + await first; releaseFirstSend.resolve(); await publisher.push(context, "flush", [createEvent("message.completed")]); - expect(blocked).toBe(false); await context.logger.destroy(); }); @@ -688,6 +687,88 @@ describe("DriverEventPublisher", () => { await context.logger.destroy(); }); + test("keeps a failed terminal settlement reserved until the same run retries it", async () => { + const attempts: DriverEventInput[][] = []; + let activeRunId: RunId | null = DRIVER_TEST_IDS.runId; + let available = false; + let seq = 0; + const context = createContext({ + currentRunId: () => activeRunId, + pushEvents: async (events) => { + attempts.push(events); + + if (!available) { + throw new Error("terminal transport unavailable"); + } + + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const closure = createEvent("message.completed"); + const terminal = createRunTerminal("run.completed"); + + await expect(publisher.pushTerminal(context, "terminal", [closure], terminal)).rejects.toThrow( + "terminal transport unavailable", + ); + activeRunId = DRIVER_TEST_IDS.secondRunId; + await expect( + publisher.pushSession(context, "session", [ + { + kind: "agent.task.updated", + payload: { active: false, status: "completed", taskId: "agent-1" }, + }, + ]), + ).rejects.toThrow("terminal settlement slot is full"); + expect(() => + publisher.pushTerminal(context, "next-run", [], { + ...terminal, + runId: DRIVER_TEST_IDS.secondRunId, + }), + ).toThrow("terminal settlement slot is full"); + + activeRunId = DRIVER_TEST_IDS.runId; + available = true; + await publisher.pushTerminal(context, "terminal.retry", [closure], terminal); + + expect(kinds(attempts)).toEqual([ + ["message.completed"], + ["message.completed"], + ["message.completed"], + ["run.completed"], + ]); + expect(attempts.flat().some((event) => event.kind === "agent.task.updated")).toBe(false); + await context.logger.destroy(); + }); + + test("does not let a session push cross a terminal installed at its await boundary", async () => { + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const session = publisher.pushSession(context, "session", [ + { + kind: "agent.task.updated", + payload: { active: false, status: "completed", taskId: "agent-1" }, + }, + ]); + const terminal = publisher.pushTerminal( + context, + "terminal", + [], + createRunTerminal("run.completed"), + ); + + await expect(session).rejects.toThrow("terminal settlement slot is full"); + await terminal; + expect(kinds(attempts)).toEqual([["run.completed"]]); + await context.logger.destroy(); + }); + test("adopts the identity of a matching generic pending closure", async () => { const attempts: DriverEventInput[][] = []; let seq = 0; @@ -1086,7 +1167,7 @@ describe("DriverEventPublisher", () => { await context.logger.destroy(); }); - test("rejects an oversized closure without delivering the run terminal", async () => { + test("rejects aggregate terminal bytes before delivering any event", async () => { let sends = 0; const context = createContext({ pushEvents: async () => { @@ -1095,23 +1176,49 @@ describe("DriverEventPublisher", () => { }, }); const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const oversized = { + const closures = ["one", "two"].map((messageId): DriverEventInput => ({ kind: "message.completed", payload: { - messageId: "message-1", - metadata: { detail: "x".repeat(32 * 1_024 * 1_024) }, + messageId, + metadata: { detail: "x".repeat(600 * 1_024) }, stopReason: "end_turn", }, - } satisfies DriverEventInput; + })); + + expect(() => + publisher.pushTerminal(context, "terminal", closures, createRunTerminal("run.completed")), + ).toThrow("run terminal batch exceeds 1048576 UTF-8 bytes"); + expect(sends).toBe(0); + await context.logger.destroy(); + }); + + test("rejects aggregate terminal count before reading payloads", async () => { + let payloadReads = 0; + let sends = 0; + const closure = { + kind: "message.completed", + get payload() { + payloadReads += 1; + return { messageId: "message-1", stopReason: "end_turn" }; + }, + } as DriverEventInput; + const context = createContext({ + pushEvents: async () => { + sends += 1; + return { accepted: [] }; + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); expect(() => publisher.pushTerminal( context, "terminal", - [createEvent("message.completed"), oversized], + Array.from({ length: 1_024 }, () => closure), createRunTerminal("run.completed"), ), - ).toThrow("Driver event queue exceeds 33554432 UTF-8 bytes"); + ).toThrow("exceeds 1024 events"); + expect(payloadReads).toBe(0); expect(sends).toBe(0); await context.logger.destroy(); }); @@ -1410,7 +1517,7 @@ describe("DriverEventPublisher", () => { ...createRunTerminal("run.completed"), runId: DRIVER_TEST_IDS.secondRunId, }), - ).toThrow("settlement slot is full"); + ).toThrow("must target the active run"); release.resolve(); await first; diff --git a/tests/driver-event-publisher-backpressure.test.ts b/tests/driver-event-publisher-backpressure.test.ts index 98c9357..be2931f 100644 --- a/tests/driver-event-publisher-backpressure.test.ts +++ b/tests/driver-event-publisher-backpressure.test.ts @@ -144,9 +144,7 @@ describe("DriverEventPublisher", () => { releaseFirstSend.resolve(); await expect(first).rejects.toThrow("socket unavailable"); - expect( - await Promise.race([recovered.promise.then(() => true), Bun.sleep(50).then(() => false)]), - ).toBe(true); + await recovered.promise; expect(attempts).toHaveLength(2); expect(attempts[1]?.map((event) => event.sourceEventId)).toEqual( attempts[0]?.map((event) => event.sourceEventId), diff --git a/tests/driver-golden-fixtures.test.ts b/tests/driver-golden-fixtures.test.ts index 6d3bcd2..5bd800a 100644 --- a/tests/driver-golden-fixtures.test.ts +++ b/tests/driver-golden-fixtures.test.ts @@ -94,6 +94,18 @@ describe("Driver golden fixtures", () => { ); }); + test.each([Number.NaN, Number.POSITIVE_INFINITY, undefined])( + "rejects non-JSON usage payload value %p at the shared event boundary", + (tokens) => { + expect( + ingestRuntimeEventInput(createRuntimeEventContext(), { + kind: "usage.updated", + payload: { tokens }, + }), + ).toMatchObject({ status: "rejected" }); + }, + ); + test("enforces the shared agent task payload contract", () => { expect( ingestRuntimeEventInput(createRuntimeEventContext(), { diff --git a/tests/driver-instance-socket-process.test.ts b/tests/driver-instance-socket-process.test.ts index f565533..82cebb0 100644 --- a/tests/driver-instance-socket-process.test.ts +++ b/tests/driver-instance-socket-process.test.ts @@ -538,6 +538,81 @@ describe("DriverProcess lifecycle", () => { expect(process.listeners("SIGTERM")).toEqual([...existingSignalListeners]); }, 15_000); + test.each(["before", "after"] as const)( + "linearizes a completed run terminal selected %s input cancellation", + async (selectionOrder) => { + globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; + RpcWebSocket.commands = [ + { + commandId: `terminal-${selectionOrder}-input`, + input: { text: "complete once" }, + kind: "input.start", + requestId: `terminal-${selectionOrder}-request`, + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: `terminal-${selectionOrder}-cancel`, + kind: "turn.cancel", + reason: "test cancellation", + }, + { + commandId: `terminal-${selectionOrder}-stop`, + kind: "session.stop", + reason: "test complete", + }, + ]; + const inputEntered = Promise.withResolvers(); + const cancellationEntered = Promise.withResolvers(); + const allowTerminalSelection = Promise.withResolvers(); + const backend = createBackend(); + backend.handleInput = async (context, _input, runId) => { + inputEntered.resolve(); + if (selectionOrder === "after") { + await allowTerminalSelection.promise; + } + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + runId, + }, + ], + }); + }; + backend.cancelActiveTurn = async () => { + cancellationEntered.resolve(); + allowTerminalSelection.resolve(); + }; + if (selectionOrder === "before") { + RpcWebSocket.delayedEventKind = "run.completed"; + } + + const run = new DriverProcess(driverBootPayload, () => backend).run(); + await inputEntered.promise; + if (selectionOrder === "before") { + await RpcWebSocket.stalled.promise; + } + await cancellationEntered.promise; + RpcWebSocket.delayedResponse.resolve(); + await expect(run).resolves.toBeUndefined(); + + const inputTerminals = (PendingWebSocket.instances[0] as RpcWebSocket).requests + .filter(({ path }) => path === "/driver/commandUpdate") + .map(({ input }) => input as { commandId: string; status: string }) + .filter( + ({ commandId, status }) => + commandId === `terminal-${selectionOrder}-input` && status !== "accepted", + ); + expect(inputTerminals).toEqual([ + expect.objectContaining({ + commandId: `terminal-${selectionOrder}-input`, + status: selectionOrder === "before" ? "completed" : "cancelled", + }), + ]); + }, + ); + test.each([ ["hello", "/driver/hello", false, 0], ["backend start", "/driverInstance/nextCommand", true, 2], @@ -634,71 +709,6 @@ describe("DriverProcess lifecycle", () => { expect(process.listeners("SIGTERM")).toEqual([...existingSignalListeners]); }); - test("automatically retries final cleanup after shutdown times out during backend start", async () => { - globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; - const existingSignalListeners = new Set(process.listeners("SIGTERM")); - const startEntered = Promise.withResolvers(); - const releaseStart = Promise.withResolvers(); - const finalCleanup = Promise.withResolvers(); - const backend = createBackend(); - let resourceActive = false; - let stopCount = 0; - let startSignal: AbortSignal | undefined; - backend.start = async (_context, signal) => { - startSignal = signal; - startEntered.resolve(); - await releaseStart.promise; - signal.throwIfAborted(); - resourceActive = true; - }; - backend.stop = async () => { - stopCount += 1; - resourceActive = false; - - if (stopCount === 3) { - finalCleanup.resolve(); - } - }; - const nativeSetTimeout = globalThis.setTimeout; - const acceleratedSetTimeout = ( - callback: (...args: unknown[]) => void, - delay?: number, - ...args: unknown[] - ) => nativeSetTimeout(callback, delay === 5_000 ? 10 : delay, ...args); - globalThis.setTimeout = acceleratedSetTimeout as typeof setTimeout; - - try { - const run = new DriverProcess(driverBootPayload, () => backend).run(); - const outcome = run.then( - () => null, - (error: unknown) => error, - ); - - await startEntered.promise; - const shutdown = process - .listeners("SIGTERM") - .find((listener) => !existingSignalListeners.has(listener)); - expect(shutdown).toBeDefined(); - shutdown?.("SIGTERM"); - await outcome; - expect(startSignal?.aborted).toBe(true); - expect(startSignal?.reason).toMatchObject({ message: "signal.sigterm" }); - releaseStart.resolve(); - - const cleaned = await Promise.race([ - finalCleanup.promise.then(() => true), - Bun.sleep(50).then(() => false), - ]); - expect(cleaned).toBe(true); - expect(stopCount).toBe(3); - expect(resourceActive).toBe(false); - expect(process.listeners("SIGTERM")).toEqual([...existingSignalListeners]); - } finally { - releaseStart.resolve(); - globalThis.setTimeout = nativeSetTimeout; - } - }); - test("propagates a heartbeat failure instead of treating it as a normal shutdown", async () => { globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; RpcWebSocket.heartbeatFails = true; @@ -1001,12 +1011,14 @@ describe("DriverProcess lifecycle", () => { }); } }; + const nativeNow = Date.now; const nativeSetTimeout = globalThis.setTimeout; const acceleratedSetTimeout = ( callback: (...args: unknown[]) => void, delay?: number, ...args: unknown[] ) => nativeSetTimeout(callback, delay === 5_000 ? 10 : delay, ...args); + Date.now = () => 0; globalThis.setTimeout = acceleratedSetTimeout as typeof setTimeout; try { @@ -1026,6 +1038,7 @@ describe("DriverProcess lifecycle", () => { expect(stopSignals[1]?.aborted).toBe(false); expect(process.listeners("SIGTERM")).toEqual([...existingSignalListeners]); } finally { + Date.now = nativeNow; globalThis.setTimeout = nativeSetTimeout; } }); diff --git a/tests/driver-instance-socket-socket.test.ts b/tests/driver-instance-socket-socket.test.ts index f37e2ac..951e0ea 100644 --- a/tests/driver-instance-socket-socket.test.ts +++ b/tests/driver-instance-socket-socket.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { DriverInstanceSocket } from "../src/infrastructure/runtime/driver-instance-socket"; +import type { DriverEventInput } from "../src/protocol/events"; import { settlePromiseWithTimeout } from "../src/utils/async"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; @@ -332,6 +333,27 @@ describe("DriverInstanceSocket lifecycle", () => { expect(closeNotifications).toEqual([[1000, "test shutdown"]]); }); + test("bounds close reasons at a complete UTF-8 character", async () => { + globalThis.WebSocket = OpenWebSocket as unknown as typeof WebSocket; + const socket = new DriverInstanceSocket(driverBootPayload, { + onClose: () => {}, + }); + await socket.connect(); + const reason = `${"x".repeat(121)}🙂diagnostic detail`; + const heartbeat = socket.heartbeat({ + at: new Date(0).toISOString(), + reason: "interval", + }); + await Promise.resolve(); + + socket.close(1000, reason); + + await expect(heartbeat).rejects.toThrow(reason); + const closeReason = PendingWebSocket.instances[0]?.closeReason; + expect(closeReason).toBe("x".repeat(121)); + expect(Buffer.byteLength(closeReason ?? "", "utf8")).toBeLessThanOrEqual(123); + }); + test("requires a fresh hello after reconnecting", async () => { globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; const socket = new DriverInstanceSocket(driverBootPayload, { @@ -406,6 +428,35 @@ describe("DriverInstanceSocket lifecycle", () => { socket.close(); }); + test("keeps durable effect settlement independent from aborted ordinary RPCs", async () => { + const socket = await connectRpcSocket(); + socket.abortPendingRequests("test shutdown"); + const signal = new AbortController().signal; + + await expect( + socket.completeExternalToolEffect( + { + commandId: "effect-command", + result: { + outputText: "done", + requestId: "effect-request", + serverId: "effect-server", + toolName: "lookup", + }, + }, + signal, + ), + ).resolves.toBeUndefined(); + await expect( + socket.markExternalToolEffectUnknown({ commandId: "unknown-command" }, signal), + ).resolves.toBeUndefined(); + + expect((PendingWebSocket.instances[0] as RpcWebSocket).paths).toEqual([ + "/driver/completeExternalToolEffect", + "/driver/markExternalToolEffectUnknown", + ]); + }); + test("uses the terminal-attempt signal to abort only that command update", async () => { globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; RpcWebSocket.stalledPath = "/driver/commandUpdate"; @@ -655,6 +706,292 @@ describe("DriverInstanceSocket lifecycle", () => { expect(result.accepted).toHaveLength(3); }); + test("linearizes each run at one fully acknowledged final terminal", async () => { + RpcWebSocket.eventBatchMaxSize = 2; + const socket = await connectRpcSocket(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + socket.beginRun(DRIVER_TEST_IDS.runId); + const delta: DriverEventInput = { + kind: "message.delta", + payload: { contentDelta: "x", messageId: "message-1", role: "agent" }, + }; + const completed: DriverEventInput = { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + }; + + await expect(socket.pushEvents({ events: [completed, delta] })).rejects.toThrow( + "must be the final event", + ); + await expect( + socket.pushEvents({ + events: [ + completed, + { + kind: "run.failed", + payload: { error: { code: "failed", message: "failed", retryable: false } }, + }, + ], + }), + ).rejects.toThrow("multiple run terminals"); + await expect( + socket.pushEvents({ events: [{ ...completed, delivery: "best_effort" }] }), + ).rejects.toThrow("must use lossless delivery"); + expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + + RpcWebSocket.acceptedEventCounts = [1, 0]; + await expect(socket.pushEvents({ events: [delta, completed] })).rejects.toThrow("no progress"); + expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + expect(socket.selectedRunEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + + await expect(socket.pushEvents({ events: [completed] })).resolves.toMatchObject({ + accepted: [{ type: "run.completed" }], + }); + expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + await expect(socket.pushEvents({ events: [delta] })).rejects.toThrow( + "cannot target a terminated run", + ); + await expect(socket.pushEvents({ events: [{ ...delta, runId: null }] })).resolves.toMatchObject( + { accepted: [{ type: "message.delta" }] }, + ); + await expect( + socket.pushEvents({ events: [{ ...delta, runId: DRIVER_TEST_IDS.secondRunId }] }), + ).rejects.toThrow("must target the active run"); + }); + + test("does not replace an active run or discard its terminal state", async () => { + const socket = await connectRpcSocket(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + socket.beginRun(DRIVER_TEST_IDS.runId); + await socket.pushEvents({ + events: [{ kind: "run.completed", payload: { stopReason: "end_turn" } }], + }); + + expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow("another run is active"); + expect(socket.currentRunId()).toBe(DRIVER_TEST_IDS.runId); + expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + + socket.endRun(DRIVER_TEST_IDS.runId); + socket.beginRun(DRIVER_TEST_IDS.secondRunId); + expect(socket.currentRunId()).toBe(DRIVER_TEST_IDS.secondRunId); + expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + }); + + test("reserves a terminal before its RPC and serializes the matching control terminal", async () => { + const socket = await connectRpcSocket(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + socket.beginRun(DRIVER_TEST_IDS.runId); + RpcWebSocket.stalledPath = "/driver/pushEvents"; + const completed: DriverEventInput = { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + }; + const failed: DriverEventInput = { + kind: "run.failed", + payload: { error: { code: "failed", message: "failed", retryable: false } }, + }; + const failure = { + code: "failed", + details: {}, + message: "failed", + retryable: false, + }; + + const eventTerminal = socket.pushEvents({ events: [completed] }); + await RpcWebSocket.stalled.promise; + expect(socket.selectedRunEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + await expect(socket.pushEvents({ events: [failed] })).rejects.toThrow( + "cannot target a terminated run", + ); + const matchingControl = socket.completeRun(); + const eventTerminalOutcome = eventTerminal.then( + () => null, + (error: unknown) => error, + ); + const matchingControlOutcome = matchingControl.then( + () => null, + (error: unknown) => error, + ); + await expect(socket.failRun(failure)).resolves.toBeUndefined(); + await Bun.sleep(0); + + const firstWire = PendingWebSocket.instances[0] as RpcWebSocket; + expect(firstWire.paths.filter((path) => path !== "/driver/hello")).toEqual([ + "/driver/pushEvents", + ]); + firstWire.close(1006, "terminal response lost"); + const eventTerminalError = await eventTerminalOutcome; + const matchingControlError = await matchingControlOutcome; + expect(eventTerminalError).toBeInstanceOf(Error); + expect(matchingControlError).toBeInstanceOf(Error); + expect(matchingControlError as Error).toHaveProperty( + "message", + expect.stringContaining("connection changed"), + ); + expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + + await socket.connect(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + await expect(socket.pushEvents({ events: [failed] })).rejects.toThrow( + "cannot target a terminated run", + ); + expect((PendingWebSocket.instances[1] as RpcWebSocket).paths).toEqual(["/driver/hello"]); + }); + + test.each([ + ["completeRun", "/driver/completeRun"], + ["failRun", "/driver/failRun"], + ] as const)("does not begin another run after an in-flight %s", async (selected, path) => { + const socket = await connectRpcSocket(); + socket.beginRun(DRIVER_TEST_IDS.runId); + const failure = { + code: "test.failure", + details: {}, + message: "failed", + retryable: false, + }; + RpcWebSocket.lostResponsePath = path; + const terminal = selected === "completeRun" ? socket.completeRun() : socket.failRun(failure); + await RpcWebSocket.stalled.promise; + + socket.endRun(DRIVER_TEST_IDS.runId); + expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow( + "control terminal has been selected", + ); + expect(socket.currentRunId()).toBeNull(); + expect(selected === "completeRun" ? socket.completeRun() : socket.failRun(failure)).toBe( + terminal, + ); + await expect( + selected === "completeRun" ? socket.failRun(failure) : socket.completeRun(), + ).resolves.toBeUndefined(); + + (PendingWebSocket.instances[0] as RpcWebSocket).close(1006, "terminal response lost"); + await expect(terminal).rejects.toThrow("terminal response lost"); + expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow( + "control terminal has been selected", + ); + }); + + test("does not redirect a queued run event after the active run ends", async () => { + const socket = await connectRpcSocket(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + socket.beginRun(DRIVER_TEST_IDS.runId); + RpcWebSocket.stalledPath = "/driver/pushEvents"; + const controller = new AbortController(); + const blocker = socket.pushEvents({ + events: [ + { + kind: "diagnostic.reported", + payload: { code: "queue.blocker", message: "block", severity: "info" }, + runId: null, + }, + ], + signal: controller.signal, + }); + await RpcWebSocket.stalled.promise; + const queuedTerminal = socket.pushEvents({ + events: [{ kind: "run.completed", payload: { stopReason: "end_turn" } }], + }); + const queuedOutcome = queuedTerminal.then( + () => null, + (error: unknown) => error, + ); + + socket.endRun(DRIVER_TEST_IDS.runId); + socket.beginRun(DRIVER_TEST_IDS.secondRunId); + controller.abort(new Error("release old run")); + await expect(blocker).rejects.toThrow(); + const queuedError = await queuedOutcome; + expect(queuedError).toBeInstanceOf(Error); + expect(queuedError as Error).toHaveProperty( + "message", + expect.stringContaining("active run changed"), + ); + expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + expect( + (PendingWebSocket.instances[0] as RpcWebSocket).paths.filter( + (path) => path === "/driver/pushEvents", + ), + ).toHaveLength(1); + + RpcWebSocket.stalledPath = null; + await expect( + socket.pushEvents({ + events: [ + { + kind: "message.delta", + payload: { contentDelta: "new", messageId: "message-2", role: "agent" }, + }, + ], + }), + ).resolves.toMatchObject({ accepted: [{ type: "message.delta" }] }); + }); + + test("does not send a queued control terminal after its run ends", async () => { + const socket = await connectRpcSocket(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + socket.beginRun(DRIVER_TEST_IDS.runId); + RpcWebSocket.stalledPath = "/driver/pushEvents"; + const controller = new AbortController(); + const blocker = socket.pushEvents({ + events: [ + { + kind: "diagnostic.reported", + payload: { code: "queue.blocker", message: "block", severity: "info" }, + runId: null, + }, + ], + signal: controller.signal, + }); + await RpcWebSocket.stalled.promise; + const oldTerminal = socket.completeRun(); + + socket.endRun(DRIVER_TEST_IDS.runId); + expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow( + "control terminal has been selected", + ); + controller.abort(new Error("release old run")); + + await expect(blocker).rejects.toThrow(); + await expect(oldTerminal).rejects.toThrow("active run changed"); + const paths = (PendingWebSocket.instances[0] as RpcWebSocket).paths; + expect(paths.filter((path) => path === "/driver/pushEvents")).toHaveLength(1); + expect(paths).not.toContain("/driver/completeRun"); + expect(socket.currentRunId()).toBeNull(); + }); + test("does not retry an unaccepted best-effort suffix", async () => { RpcWebSocket.eventBatchMaxSize = 2; RpcWebSocket.acceptedEventCounts = [1]; diff --git a/tests/driver-logger.test.ts b/tests/driver-logger.test.ts index 7fb24d5..fb5d519 100644 --- a/tests/driver-logger.test.ts +++ b/tests/driver-logger.test.ts @@ -5,12 +5,14 @@ import type { DriverInstanceSocket } from "../src/infrastructure/runtime/driver- import type { DriverBootPayload } from "../src/protocol/boot"; import type { DriverLogBatchInput } from "../src/protocol/orpc"; -const FLUSH_INTERVAL_MS = 200; - function createBootPayload(): DriverBootPayload { return { driverInstanceId: "01J00000000000000000000DRV", - sandboxId: "01J00000000000000000000SBX", + execution: { + session: { + context: { sandboxId: "01J00000000000000000000SBX" }, + }, + }, } as DriverBootPayload; } @@ -42,10 +44,6 @@ function createFakeSocket(): FakeSocket { return state; } -async function waitForFlushWindow(): Promise { - await Bun.sleep(FLUSH_INTERVAL_MS + 100); -} - describe("createDriverLogger", () => { test("holds log batches until the uplink gate opens", async () => { const fake = createFakeSocket(); @@ -54,11 +52,16 @@ describe("createDriverLogger", () => { logger.info("driver.runtime.boot.loaded"); logger.info("driver.runtime.hello.sending"); - await waitForFlushWindow(); + let flushed = false; + const flush = logger.flush().then(() => { + flushed = true; + }); + await Promise.resolve(); + expect(flushed).toBe(false); expect(fake.batches).toHaveLength(0); uplink.open(); - await logger.flush(); + await flush; const sent = fake.batches.flatMap((batch) => batch.logs); expect(sent.map((entry) => entry.message)).toEqual([ diff --git a/tests/driver-permission-broker.test.ts b/tests/driver-permission-broker.test.ts index 559d83c..e1e323c 100644 --- a/tests/driver-permission-broker.test.ts +++ b/tests/driver-permission-broker.test.ts @@ -123,6 +123,29 @@ describe("DriverPermissionBroker", () => { await expect(first).resolves.toBe("allow_once"); }); + test("rejects a request whose delivery outlives its run generation", async () => { + const deliveryEntered = Promise.withResolvers(); + const releaseDelivery = Promise.withResolvers(); + const broker = new DriverPermissionBroker(() => null); + let ownsRun = true; + const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, + pushEvents: async ({ events }) => { + deliveryEntered.resolve(); + await releaseDelivery.promise; + return acceptEvents(events); + }, + }; + + const request = broker.request(socket, permissionInput, undefined, () => ownsRun); + await deliveryEntered.promise; + ownsRun = false; + releaseDelivery.resolve(); + + await expect(request).resolves.toBe("reject_once"); + expect(broker.hasPending()).toBe(false); + }); + test("bounds the number of pending requests", async () => { const broker = new DriverPermissionBroker(() => null, { maxPendingRequestBytes: 1_024, @@ -804,6 +827,51 @@ describe("DriverPermissionBroker", () => { ]); }); + test("gives a cancellation retry a fresh wait budget", async () => { + const requestedPublishing = Promise.withResolvers(); + const releaseRequested = Promise.withResolvers(); + const socket: DriverRuntimeEventPort = { + currentRunId: () => runId, + pushEvents: async ({ events }) => { + if (events.some((event) => event.kind === "permission.requested")) { + requestedPublishing.resolve(); + await releaseRequested.promise; + } + return acceptEvents(events); + }, + }; + const broker = new DriverPermissionBroker(() => null, { + eventDeliveryTimeoutMs: 10, + }); + const request = broker.request(socket, permissionInput); + const requestOutcome = request.then( + () => null, + (error: unknown) => error, + ); + + await requestedPublishing.promise; + const first = broker.rejectAllAndWait(); + await expect(first).rejects.toThrow("Driver permission cancellation timed out"); + + const retry = broker.rejectAllAndWait(); + expect(retry).not.toBe(first); + let retrySettled = false; + void retry.then( + () => { + retrySettled = true; + }, + () => { + retrySettled = true; + }, + ); + await Promise.resolve(); + expect(retrySettled).toBe(false); + + releaseRequested.resolve(); + expect(await requestOutcome).toBeInstanceOf(PermissionEventDeliveryError); + await expect(retry).resolves.toBeUndefined(); + }); + test("bounds ordinary cancellation delivery below the active-turn grace", async () => { const requestedPublishing = Promise.withResolvers(); const pushedKinds: string[] = []; diff --git a/tests/driver-runtime-boundary-fixtures.ts b/tests/driver-runtime-boundary-fixtures.ts index 7b961ee..8303474 100644 --- a/tests/driver-runtime-boundary-fixtures.ts +++ b/tests/driver-runtime-boundary-fixtures.ts @@ -7,7 +7,12 @@ import type { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state import type { AgentDriverMcpPort } from "../src/host-ports"; import { createBufferedSinkLogger } from "../src/observability"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; -import type { RuntimeCommand } from "../src/runtime-command"; +import type { + McpExecuteCommand, + McpExternalToolEffectExecution, + McpExternalToolExecutionResult, + RuntimeCommand, +} from "../src/runtime-command"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; export { DRIVER_TEST_IDS }; @@ -153,7 +158,12 @@ export function createBackend(): RecordingBackend { export function createDispatcher(input: { backend: AgentDriverBackend; isShuttingDown?: () => boolean; - mcpExecute?: AgentDriverMcpPort["execute"]; + mcpExecute?: ( + command: McpExecuteCommand, + signal: AbortSignal, + effect: McpExternalToolEffectExecution, + ) => Promise; + mcpPrepare?: AgentDriverMcpPort["prepare"]; permissionRequests?: DriverPermissionBroker; rememberRunFailure?: (error: Parameters[0]) => void; runtimeState: DriverRuntimeStateMachine; @@ -200,13 +210,20 @@ export function createDispatcher(input: { }, }, mcp: { - execute: - input.mcpExecute ?? - (async (command) => ({ - outputText: `ran ${command.toolName}`, - requestId: command.requestId, - serverId: command.serverId, - toolName: command.toolName, + prepare: + input.mcpPrepare ?? + (async (command, signal) => ({ + execute: (effect) => + ( + input.mcpExecute ?? + (async () => ({ + outputText: `ran ${command.toolName}`, + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + })) + )(command, signal, effect), + async [Symbol.asyncDispose]() {}, })), }, }, diff --git a/tests/driver-runtime-boundary-state.test.ts b/tests/driver-runtime-boundary-state.test.ts index a0ebf8e..8c99681 100644 --- a/tests/driver-runtime-boundary-state.test.ts +++ b/tests/driver-runtime-boundary-state.test.ts @@ -3,18 +3,57 @@ import { describe, expect, test } from "bun:test"; import { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; import { createTimingEvent } from "../src/core/driver-runtime-timing"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; +import type { CredentialId, McpServerId } from "../src/protocol/boot"; import { parseDriverEventEnvelope } from "../src/protocol/events"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; -import type { RuntimeCommand } from "../src/runtime-command"; +import type { DriverStartInput } from "../src/protocol/start"; +import type { McpExecuteCommand, RuntimeCommand } from "../src/runtime-command"; +import { prepareRemoteHttpMcpCommand } from "../src/runtimes/mcp/remote-http-mcp-executor"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; import { FakeDriverRuntimeIo, + bootPayload, createBackend, createDispatcher, waitForUpdate, } from "./driver-runtime-boundary-fixtures"; +const MCP_PREFLIGHT_SERVER_ID = "01J00000000000000000000020" as McpServerId; +const MCP_PREFLIGHT_CREDENTIAL_ID = "01J00000000000000000000021" as CredentialId; + +function withMcpServers( + mcpServers: DriverStartInput["execution"]["session"]["mcpServers"], +): DriverStartInput { + return { + ...bootPayload, + execution: { + ...bootPayload.execution, + session: { + ...bootPayload.execution.session, + mcpServers, + }, + }, + }; +} + +class EffectLedgerRecordingSocket extends FakeDriverRuntimeIo { + claimCount = 0; + unknownCount = 0; + + override async claimExternalToolEffect( + input: Parameters[0], + signal: AbortSignal, + ): ReturnType { + this.claimCount += 1; + return super.claimExternalToolEffect(input, signal); + } + + override async markExternalToolEffectUnknown(): Promise { + this.unknownCount += 1; + } +} + describe("driver runtime boundary", () => { test("rejects an invalid runtime transition", () => { const state = new DriverRuntimeStateMachine("created"); @@ -454,27 +493,39 @@ describe("driver runtime boundary", () => { await first.logger.destroy(); const secondSocket = new FakeDriverRuntimeIo([structuredClone(command)]); + let secondClaims = 0; + let secondPreparations = 0; const second = createDispatcher({ backend: createBackend(), isShuttingDown: () => secondSocket.isDrained(), - mcpExecute: async () => { - providerCalls += 1; - throw new Error("provider must not run during terminal redelivery"); + mcpPrepare: async () => { + secondPreparations += 1; + return { + async execute() { + throw new Error("completed effect must not execute"); + }, + async [Symbol.asyncDispose]() {}, + }; }, runtimeState: new DriverRuntimeStateMachine("ready"), }); // The default fixture ledger has no cross-process state, so carry the // persisted effect receipt through the fresh Dispatcher explicitly. - secondSocket.claimExternalToolEffect = async () => ({ - effectId: "effect-1", - kind: "completed" as const, - result: effectResult!, - }); + secondSocket.claimExternalToolEffect = async () => { + secondClaims += 1; + return { + effectId: "effect-1", + kind: "completed" as const, + result: effectResult!, + }; + }; await second.dispatcher.run(secondSocket, second.logger); await second.logger.destroy(); expect(providerCalls).toBe(1); + expect(secondPreparations).toBe(1); + expect(secondClaims).toBe(1); expect(secondSocket.updates).toEqual([ { commandId: command.commandId, status: "accepted" }, { commandId: command.commandId, result: effectResult, status: "completed" }, @@ -500,17 +551,28 @@ describe("driver runtime boundary", () => { toolName: "createIssue", }; const socket = new FakeDriverRuntimeIo([command]); - socket.claimExternalToolEffect = async () => ({ - effectId: "01J0000000000000000000000Z", - kind: "unknown" as const, - }); - let providerCalls = 0; + let claims = 0; + let executions = 0; + let preparations = 0; + socket.claimExternalToolEffect = async () => { + claims += 1; + return { + effectId: "01J0000000000000000000000Z", + kind: "unknown" as const, + }; + }; const runtime = createDispatcher({ backend: createBackend(), isShuttingDown: () => socket.isDrained(), - mcpExecute: async () => { - providerCalls += 1; - throw new Error("provider must not run for an unknown effect"); + mcpPrepare: async () => { + preparations += 1; + return { + async execute() { + executions += 1; + throw new Error("unknown effect must not execute"); + }, + async [Symbol.asyncDispose]() {}, + }; }, runtimeState: new DriverRuntimeStateMachine("ready"), }); @@ -518,7 +580,9 @@ describe("driver runtime boundary", () => { await runtime.dispatcher.run(socket, runtime.logger); await runtime.logger.destroy(); - expect(providerCalls).toBe(0); + expect(preparations).toBe(1); + expect(claims).toBe(1); + expect(executions).toBe(0); expect(socket.updates).toMatchObject([ { commandId: command.commandId, status: "accepted" }, { @@ -533,7 +597,432 @@ describe("driver runtime boundary", () => { ]); }); - test("fences an in-flight MCP effect with a live signal when a turn is cancelled", async () => { + test("disposes MCP preparation when the claim CAS observes a completed effect", async () => { + const command: McpExecuteCommand = { + argumentsJson: "{}", + commandId: "claim-race-completed", + kind: "mcp.execute", + requestId: "claim-race-request", + serverId: MCP_PREFLIGHT_SERVER_ID, + toolCallId: "claim-race-tool", + toolName: "lookup", + }; + const result = { + outputText: "already completed", + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }; + const socket = new FakeDriverRuntimeIo([command]); + socket.claimExternalToolEffect = async () => ({ + effectId: "claim-race-effect", + kind: "completed" as const, + result, + }); + let disposals = 0; + let executions = 0; + const disposed = Promise.withResolvers(); + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => ({ + async execute() { + executions += 1; + throw new Error("completed claim must not execute"); + }, + async [Symbol.asyncDispose]() { + disposals += 1; + disposed.resolve(); + }, + }), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await disposed.promise; + await runtime.logger.destroy(); + + expect(executions).toBe(0); + expect(disposals).toBe(1); + expect(socket.updates).toContainEqual({ + commandId: command.commandId, + result, + status: "completed", + }); + }); + + test.each(["executed", "completed claim"] as const)( + "keeps a durable %s MCP result completed when its event publication fails", + async (settlement) => { + const command: McpExecuteCommand = { + argumentsJson: "{}", + commandId: `durable-event-failure-${settlement.replaceAll(" ", "-")}`, + kind: "mcp.execute", + requestId: "durable-event-failure-request", + serverId: MCP_PREFLIGHT_SERVER_ID, + toolCallId: "durable-event-failure-tool", + toolName: "lookup", + }; + const result = { + outputText: "durable result", + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }; + const durable = Promise.withResolvers(); + const completedEventEntered = Promise.withResolvers(); + const releaseCompletedEvent = Promise.withResolvers(); + + class FailingCompletedEventSocket extends FakeDriverRuntimeIo { + unknownCount = 0; + + override async claimExternalToolEffect(): ReturnType< + FakeDriverRuntimeIo["claimExternalToolEffect"] + > { + if (settlement === "completed claim") { + durable.resolve(); + return { effectId: "durable-effect", kind: "completed", result }; + } + return { + attempt: 1, + effectId: "durable-effect", + idempotencyKey: "durable-effect", + kind: "execute", + }; + } + + override async completeExternalToolEffect(): Promise { + durable.resolve(); + } + + override async markExternalToolEffectUnknown(): Promise { + this.unknownCount += 1; + } + + override async pushEvents( + input: Parameters[0], + ): ReturnType { + if ( + input.events.some( + (event) => + event.kind === "tool.call.updated" && + typeof event.payload === "object" && + event.payload !== null && + "status" in event.payload && + event.payload.status === "completed", + ) + ) { + completedEventEntered.resolve(); + await releaseCompletedEvent.promise; + throw new Error("completed tool event delivery failed"); + } + return super.pushEvents(input); + } + } + + const socket = new FailingCompletedEventSocket([command]); + let providerCalls = 0; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpExecute: async () => { + providerCalls += 1; + return result; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + const runTask = runtime.dispatcher.run(socket, runtime.logger); + + await durable.promise; + await completedEventEntered.promise; + expect(socket.updates.filter(({ status }) => status !== "accepted")).toEqual([]); + releaseCompletedEvent.resolve(); + await runTask; + await waitForUpdate( + socket, + (update) => update.commandId === command.commandId && update.status === "completed", + ); + await runtime.logger.destroy(); + + expect(providerCalls).toBe(settlement === "executed" ? 1 : 0); + expect(socket.unknownCount).toBe(0); + expect(socket.updates.filter(({ status }) => status !== "accepted")).toEqual([ + { commandId: command.commandId, result, status: "completed" }, + ]); + }, + ); + + test.each([ + [ + "invalid arguments", + withMcpServers([ + { + authType: "bearer", + authorizationState: "active", + credentialId: MCP_PREFLIGHT_CREDENTIAL_ID, + credentialScope: "session", + credentialStatus: "active", + name: "Test MCP", + proxyGrantId: "test-grant", + proxyUrl: "https://mcp.invalid.test", + serverId: MCP_PREFLIGHT_SERVER_ID, + }, + ]), + "{", + MCP_PREFLIGHT_SERVER_ID, + "Invalid MCP tool arguments", + ], + ["missing server", withMcpServers([]), "{}", MCP_PREFLIGHT_SERVER_ID, "not configured"], + [ + "disabled server", + withMcpServers([ + { + authType: "bearer", + authorizationState: "disabled", + credentialScope: "session", + credentialStatus: "disabled", + name: "Test MCP", + serverId: MCP_PREFLIGHT_SERVER_ID, + }, + ]), + "{}", + MCP_PREFLIGHT_SERVER_ID, + "disabled for this session", + ], + ] as const)( + "rejects %s before claiming an external effect", + async (_name, payload, argumentsJson, serverId, message) => { + const command: McpExecuteCommand = { + argumentsJson, + commandId: `preflight-${_name.replaceAll(" ", "-")}`, + kind: "mcp.execute", + requestId: "preflight-request", + serverId, + toolCallId: "preflight-tool", + toolName: "lookup", + }; + + const socket = new EffectLedgerRecordingSocket([command]); + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: (input, signal) => prepareRemoteHttpMcpCommand(payload, input, signal), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate( + socket, + (update) => update.commandId === command.commandId && update.status === "failed", + ); + await runtime.logger.destroy(); + const failure = socket.updates.find( + (update) => update.commandId === command.commandId && update.status === "failed", + ); + + expect(failure?.error?.message).toContain(message); + expect(socket.claimCount).toBe(0); + expect(socket.unknownCount).toBe(0); + }, + ); + + test("does not claim an external effect when MCP connection setup fails", async () => { + let requestCount = 0; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch() { + requestCount += 1; + return new Response("unavailable", { status: 503 }); + }, + }); + const payload = withMcpServers([ + { + authType: "bearer", + authorizationState: "active", + credentialId: MCP_PREFLIGHT_CREDENTIAL_ID, + credentialScope: "session", + credentialStatus: "active", + name: "Test MCP", + proxyGrantId: "test-grant", + proxyUrl: `http://${server.hostname}:${server.port}`, + serverId: MCP_PREFLIGHT_SERVER_ID, + }, + ]); + const command: McpExecuteCommand = { + argumentsJson: "{}", + commandId: "preflight-connect-failure", + kind: "mcp.execute", + requestId: "preflight-connect-request", + serverId: MCP_PREFLIGHT_SERVER_ID, + toolCallId: "preflight-connect-tool", + toolName: "lookup", + }; + const socket = new EffectLedgerRecordingSocket([command]); + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: (input, signal) => prepareRemoteHttpMcpCommand(payload, input, signal), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + try { + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate( + socket, + (update) => update.commandId === command.commandId && update.status === "failed", + ); + const failure = socket.updates.find( + (update) => update.commandId === command.commandId && update.status === "failed", + ); + + expect(requestCount).toBeGreaterThan(0); + expect(failure?.error?.message).toContain("HTTP 503"); + expect(socket.claimCount).toBe(0); + expect(socket.unknownCount).toBe(0); + } finally { + await runtime.logger.destroy(); + await server.stop(true); + } + }); + + test.each([ + ["before prepare", 0, 0], + ["during prepare", 0, 0], + ["during claim", 1, 1], + ] as const)("linearizes MCP cancellation %s", async (stage, expectedClaims, expectedUnknowns) => { + const command: McpExecuteCommand = { + argumentsJson: "{}", + commandId: `cancel-${stage.replace(" ", "-")}`, + kind: "mcp.execute", + requestId: "cancel-preparation-request", + serverId: MCP_PREFLIGHT_SERVER_ID, + toolCallId: "cancel-preparation-tool", + toolName: "lookup", + }; + const boundaryEntered = Promise.withResolvers(); + const releaseBoundary = Promise.withResolvers(); + + class CancellationSocket extends EffectLedgerRecordingSocket { + #blocked = false; + + override async claimExternalToolEffect( + input: Parameters[0], + signal: AbortSignal, + ): ReturnType { + if (stage !== "during claim") { + return super.claimExternalToolEffect(input, signal); + } + this.claimCount += 1; + boundaryEntered.resolve(); + return new Promise((_, reject) => { + signal.addEventListener("abort", () => reject(signal.reason), { once: true }); + }); + } + + override async pushEvents( + input: Parameters[0], + ): ReturnType { + if (stage === "before prepare" && !this.#blocked) { + this.#blocked = true; + boundaryEntered.resolve(); + await releaseBoundary.promise; + } + return super.pushEvents(input); + } + } + + const socket = new CancellationSocket([command]); + const shutdown = new AbortController(); + let executions = 0; + let preparations = 0; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => shutdown.signal.aborted, + mcpPrepare: async () => { + preparations += 1; + if (stage === "during prepare") { + boundaryEntered.resolve(); + await releaseBoundary.promise; + } + return { + async execute() { + executions += 1; + throw new Error("cancelled preparation must not execute"); + }, + async [Symbol.asyncDispose]() {}, + }; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + shutdownSignal: shutdown.signal, + }); + const runTask = runtime.dispatcher.run(socket, runtime.logger); + + await boundaryEntered.promise; + shutdown.abort(new Error("test cancellation")); + releaseBoundary.resolve(); + await runTask; + await runtime.logger.destroy(); + + expect(preparations).toBe(stage === "before prepare" ? 0 : 1); + expect(executions).toBe(0); + expect(socket.claimCount).toBe(expectedClaims); + expect(socket.unknownCount).toBe(expectedUnknowns); + expect(socket.updates).toContainEqual({ + commandId: command.commandId, + status: "cancelled", + }); + }); + + test("fences an MCP claim when its acknowledgement is lost", async () => { + const command: McpExecuteCommand = { + argumentsJson: "{}", + commandId: "lost-claim-ack", + kind: "mcp.execute", + requestId: "lost-claim-request", + serverId: MCP_PREFLIGHT_SERVER_ID, + toolCallId: "lost-claim-tool", + toolName: "lookup", + }; + + class LostClaimSocket extends FakeDriverRuntimeIo { + fenceSignalAborted: boolean | null = null; + unknownCount = 0; + + override async claimExternalToolEffect(): ReturnType< + FakeDriverRuntimeIo["claimExternalToolEffect"] + > { + throw new Error("claim acknowledgement lost"); + } + + override async markExternalToolEffectUnknown( + _input: Parameters[0], + signal: AbortSignal, + ): Promise { + this.unknownCount += 1; + this.fenceSignalAborted = signal.aborted; + } + } + + const socket = new LostClaimSocket([command]); + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate( + socket, + (update) => update.commandId === command.commandId && update.status === "failed", + ); + await runtime.logger.destroy(); + + expect(socket.unknownCount).toBe(1); + expect(socket.fenceSignalAborted).toBe(false); + }); + + test("fences an in-flight MCP effect after the shutdown signal aborts", async () => { const mcpCommand: RuntimeCommand = { argumentsJson: '{"title":"cancel safely"}', commandId: "cancelled-effect-command", @@ -543,15 +1032,10 @@ describe("driver runtime boundary", () => { toolCallId: "tool-cancelled-effect", toolName: "createIssue", }; - const cancelCommand: RuntimeCommand = { - commandId: "cancelled-effect-turn", - kind: "turn.cancel", - reason: "viewer.cancelled", - }; const mcpStarted = Promise.withResolvers(); class CancellationSocket extends FakeDriverRuntimeIo { - readonly #nextCommand = Promise.withResolvers(); + readonly #nextCommand = Promise.withResolvers(); #readFirstCommand = true; #drained = false; fenceSignalAborted: boolean | null = null; @@ -582,13 +1066,15 @@ describe("driver runtime boundary", () => { return this.#nextCommand.promise; } - sendCancellation(): void { + stop(): void { this.#drained = true; - this.#nextCommand.resolve(cancelCommand); + this.#nextCommand.resolve(null); } } const socket = new CancellationSocket(); + const shutdown = new AbortController(); + const runtimeState = new DriverRuntimeStateMachine("ready"); const runtime = createDispatcher({ backend: createBackend(), isShuttingDown: () => socket.isDrained(), @@ -598,12 +1084,15 @@ describe("driver runtime boundary", () => { signal.addEventListener("abort", () => reject(signal.reason), { once: true }); }); }, - runtimeState: new DriverRuntimeStateMachine("ready"), + runtimeState, + shutdownSignal: shutdown.signal, }); const runTask = runtime.dispatcher.run(socket, runtime.logger); await mcpStarted.promise; - socket.sendCancellation(); + runtimeState.enter("stopping"); + socket.stop(); + shutdown.abort(new Error("test shutdown")); await runTask; await runtime.logger.destroy(); @@ -611,9 +1100,113 @@ describe("driver runtime boundary", () => { expect(socket.fencedCommandId).toBe(mcpCommand.commandId); expect(socket.updates).toMatchObject([ { commandId: mcpCommand.commandId, status: "accepted" }, - { commandId: cancelCommand.commandId, status: "accepted" }, { commandId: mcpCommand.commandId, status: "cancelled" }, - { commandId: cancelCommand.commandId, status: "completed" }, + ]); + }); + + test("persists a known MCP result after shutdown with a fresh retry budget", async () => { + const command: McpExecuteCommand = { + argumentsJson: "{}", + commandId: "known-result-during-shutdown", + kind: "mcp.execute", + requestId: "known-result-request", + serverId: MCP_PREFLIGHT_SERVER_ID, + toolCallId: "known-result-tool", + toolName: "lookup", + }; + const providerStarted = Promise.withResolvers(); + const releaseProvider = Promise.withResolvers(); + + class CompletionSocket extends FakeDriverRuntimeIo { + readonly #nextCommand = Promise.withResolvers(); + #readFirstCommand = true; + #drained = false; + completionAttempts = 0; + readonly completionSignals: boolean[] = []; + unknownCount = 0; + + constructor() { + super([]); + } + + override isDrained(): boolean { + return this.#drained; + } + + override nextCommand(_signal: AbortSignal): Promise { + if (this.#readFirstCommand) { + this.#readFirstCommand = false; + return Promise.resolve(command); + } + + return this.#nextCommand.promise; + } + + override async completeExternalToolEffect( + _input: Parameters[0], + signal: AbortSignal, + ): Promise { + this.completionAttempts += 1; + this.completionSignals.push(signal.aborted); + if (this.completionAttempts === 1) { + throw new Error("completion acknowledgement lost"); + } + } + + override async markExternalToolEffectUnknown(): Promise { + this.unknownCount += 1; + } + + stop(): void { + this.#drained = true; + this.#nextCommand.resolve(null); + } + } + + const socket = new CompletionSocket(); + const shutdown = new AbortController(); + const runtimeState = new DriverRuntimeStateMachine("ready"); + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpExecute: async (input) => { + providerStarted.resolve(); + await releaseProvider.promise; + return { + outputText: "known result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + }, + runtimeState, + shutdownSignal: shutdown.signal, + }); + const runTask = runtime.dispatcher.run(socket, runtime.logger); + + await providerStarted.promise; + runtimeState.enter("stopping"); + socket.stop(); + shutdown.abort(new Error("test shutdown")); + releaseProvider.resolve(); + await runTask; + await runtime.logger.destroy(); + + expect(socket.completionAttempts).toBe(2); + expect(socket.completionSignals).toEqual([false, false]); + expect(socket.unknownCount).toBe(0); + expect(socket.updates).toMatchObject([ + { commandId: command.commandId, status: "accepted" }, + { + commandId: command.commandId, + result: { + outputText: "known result", + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }, + status: "completed", + }, ]); }); diff --git a/tests/fixtures/providers/acp/cases/turn-text-tool-usage.json b/tests/fixtures/providers/acp/cases/turn-text-tool-usage.json index 6a18333..7f92b6f 100644 --- a/tests/fixtures/providers/acp/cases/turn-text-tool-usage.json +++ b/tests/fixtures/providers/acp/cases/turn-text-tool-usage.json @@ -152,13 +152,6 @@ "cachedWriteTokens": 1, "inputTokens": 4, "outputTokens": 2, - "raw": { - "cachedReadTokens": 5, - "cachedWriteTokens": 1, - "inputTokens": 4, - "outputTokens": 2, - "totalTokens": 12 - }, "source": "prompt_response", "totalTokens": 12, "usageContract": "anthropic_bucketed" diff --git a/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json b/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json index c7e2b13..6837e3f 100644 --- a/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json +++ b/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json @@ -180,15 +180,15 @@ }, { "kind": "message.started", - "payload": { "messageId": "", "role": "agent" }, - "sourceEventId": "openai.message.started:" + "payload": { "messageId": "", "role": "agent" }, + "sourceEventId": "openai.message.started:" }, { "kind": "item.started", "payload": { "itemId": "agent-call-1", "itemType": "tool_call", - "parentMessageId": "", + "parentMessageId": "", "title": "Spawn agent" }, "sourceEventId": "openai.item.started:turn-root:agent-call-1" @@ -197,7 +197,7 @@ "kind": "tool.call.updated", "payload": { "kind": "tool", - "parentMessageId": "", + "parentMessageId": "", "status": "running", "title": "Spawn agent", "toolCallId": "agent-call-1" @@ -239,14 +239,14 @@ "kind": "message.added", "payload": { "content": "PARENT_OK: CHILD_A + CHILD_B", - "messageId": "", + "messageId": "", "role": "agent" }, "sourceEventId": "openai.item.completed:turn-root:message-parent:0" }, { "kind": "message.completed", - "payload": { "messageId": "", "role": "agent" }, + "payload": { "messageId": "", "role": "agent" }, "sourceEventId": "openai.item.completed:turn-root:message-parent:1" }, { @@ -262,7 +262,7 @@ "turnId": "turn-root" }, "payload": { - "finalMessageId": "", + "finalMessageId": "", "stopReason": "end_turn" }, "runId": "", diff --git a/tests/mcp-server-key.test.ts b/tests/mcp-server-key.test.ts new file mode 100644 index 0000000..2f39b28 --- /dev/null +++ b/tests/mcp-server-key.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test"; + +import type { DriverBootMcpServer } from "../src/protocol/boot"; +import type { CredentialId, McpServerId } from "../src/protocol/boot/host-ids"; +import { toMcpServerKey } from "../src/runtimes/mcp/server-key"; + +function server(name: string): DriverBootMcpServer { + return { + authorizationState: "active", + authType: "bearer", + credentialId: "01J00000000000000000000000" as CredentialId, + credentialScope: "mcp", + credentialStatus: "active", + name, + proxyGrantId: "grant", + proxyUrl: "https://mcp.test", + serverId: "01J00000000000000000000001" as McpServerId, + }; +} + +describe("toMcpServerKey", () => { + test.each(["__proto__", "constructor", "prototype"])( + "uses the stable server id for unsafe object key %s", + (name) => { + expect(toMcpServerKey(server(name), new Set())).toBe("01J00000000000000000000001"); + }, + ); +}); diff --git a/tests/openai-app-server-auth-state.test.ts b/tests/openai-app-server-auth-state.test.ts index a56fc04..388aee2 100644 --- a/tests/openai-app-server-auth-state.test.ts +++ b/tests/openai-app-server-auth-state.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -44,8 +44,12 @@ afterEach(async () => { }); describe("OpenAI app-server auth state", () => { - test("skips unchanged API key auth writes", async () => { + test("atomically replaces API key auth symlinks with a private file", async () => { const runtimeHome = await createRuntimeHome(); + const authJsonPath = join(runtimeHome, "auth.json"); + const symlinkTarget = join(runtimeHome, "auth-target.json"); + await writeFile(symlinkTarget, "untouched\n"); + await symlink(symlinkTarget, authJsonPath); const input = { env: { OPENAI_API_KEY: "openai-key", @@ -57,10 +61,28 @@ describe("OpenAI app-server auth state", () => { hasApiKey: true, written: true, }); - await expect(materializeOpenAiApiKeyAuthState(input)).resolves.toMatchObject({ - hasApiKey: true, - written: false, + const authStat = await lstat(authJsonPath); + expect(authStat.isSymbolicLink()).toBe(false); + expect(authStat.mode & 0o777).toBe(0o600); + expect(JSON.parse(await readFile(authJsonPath, "utf8"))).toMatchObject({ + OPENAI_API_KEY: "openai-key", + auth_mode: "apikey", }); + expect(await readFile(symlinkTarget, "utf8")).toBe("untouched\n"); + expect((await readdir(runtimeHome)).filter((name) => name.endsWith(".tmp"))).toEqual([]); + }); + + test("removes the private temporary file when auth replacement fails", async () => { + const runtimeHome = await createRuntimeHome(); + await mkdir(join(runtimeHome, "auth.json")); + + await expect( + materializeOpenAiApiKeyAuthState({ + env: { OPENAI_API_KEY: "openai-key" }, + runtimeHome, + }), + ).rejects.toThrow(); + expect(await readdir(runtimeHome)).toEqual(["auth.json"]); }); test("writes model provider config for OpenAI-compatible credentials", async () => { diff --git a/tests/openai-app-server-client.test.ts b/tests/openai-app-server-client.test.ts index f31e74f..dbd0073 100644 --- a/tests/openai-app-server-client.test.ts +++ b/tests/openai-app-server-client.test.ts @@ -242,6 +242,53 @@ setInterval(() => {}, 1000); } }); + test("handles an admitted notification before a later malformed frame", async () => { + const notificationEntered = Promise.withResolvers(); + const notificationGate = Promise.withResolvers(); + let notificationCompleted = false; + const harness = await createClientHarness( + () => ` +let buffer = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { + buffer += chunk; + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const request = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (request.method === "initialize") { + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); + } else if (request.method === "initialized") { + process.stdout.write( + JSON.stringify({ method: "skills/changed", params: {} }) + "\\nnot-json\\n", + ); + } + } +}); +setInterval(() => {}, 1000); +`, + async () => { + notificationEntered.resolve(); + await notificationGate.promise; + notificationCompleted = true; + }, + ); + + try { + await harness.client.start(); + await notificationEntered.promise; + expect(harness.protocolErrors).toEqual([]); + + notificationGate.resolve(); + expect((await harness.protocolError).message).toContain("stdout is not valid JSON"); + expect(notificationCompleted).toBe(true); + } finally { + notificationGate.resolve(); + await harness.client.stop(); + await harness.logger.destroy(); + } + }); + test.each([ [ "both result and error", @@ -488,17 +535,23 @@ setInterval(() => {}, 1000); } }); - test("rejects a child process spawn failure without an unhandled error", async () => { - const harness = await createClientHarness(() => ""); - process.env["MOSOO_OPENAI_RUNTIME_EXECUTABLE"] = join(harness.directory, "missing"); + test.each(["asynchronous", "synchronous"] as const)( + "rejects a child process %s spawn failure without retaining ownership", + async (failure) => { + const harness = await createClientHarness(() => ""); + process.env["MOSOO_OPENAI_RUNTIME_EXECUTABLE"] = + failure === "asynchronous" ? join(harness.directory, "missing") : "invalid\0executable"; - try { - await expect(harness.client.start()).rejects.toThrow(); - } finally { - await harness.client.stop(); - await harness.logger.destroy(); - } - }); + try { + await expect(harness.client.start()).rejects.toThrow(); + await expect(harness.client.stop()).resolves.toBeUndefined(); + await expect(harness.client.stop()).resolves.toBeUndefined(); + } finally { + await harness.client.stop(); + await harness.logger.destroy(); + } + }, + ); test.each(["unavailable", "error"] as const)( "fails closed when the process-tree watchdog is %s", @@ -975,10 +1028,10 @@ setInterval(() => {}, 1000); try { await harness.client.start(); await permissionStarted.promise; - harness.client.abortServerRequests(new Error("turn cancelled")); + const abort = harness.client.abortServerRequests(new Error("turn cancelled")); await permissionAborted.promise; permissionGate.resolve(); - await Bun.sleep(25); + await abort; expect(await Bun.file(join(harness.directory, "late-response.json")).exists()).toBe(false); } finally { @@ -1078,7 +1131,6 @@ process.stdin.on("data", (chunk) => { while (!(await Bun.file(join(harness.directory, "leader-exited")).exists())) { await Bun.sleep(5); } - await Bun.sleep(25); const stop = harness.client.stop(); void stop.catch(() => {}); @@ -1180,7 +1232,7 @@ setInterval(() => {}, 1000); }); await expect(request).rejects.toThrow(`app-server exited with ${expectedExit}`); - await harness.client.drainServerMessages(); + await expect(harness.client.drainServerMessages()).rejects.toThrow(expectedExit); expect(harness.protocolErrors).toHaveLength(1); expect(harness.protocolErrors[0]?.message).toContain(expectedExit); } finally { @@ -1606,32 +1658,36 @@ setInterval(() => {}, 1000); } }); - test("applies backpressure to a bounded multi-agent notification burst", async () => { + test.each([ + ["backpressures below", 900, false], + ["rejects above", 1_025, true], + ] as const)("%s the 1024-message queue limit", async (_label, messageCount, rejected) => { const firstNotification = Promise.withResolvers(); const notificationGate = Promise.withResolvers(); let handled = 0; const harness = await createClientHarness( - () => ` + (directory) => ` +import { writeFileSync } from "node:fs"; let buffer = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { buffer += chunk; - const newline = buffer.indexOf("\\n"); - if (newline < 0) return; - const request = JSON.parse(buffer.slice(0, newline)); - buffer = buffer.slice(newline + 1); - if (request.id === undefined) return; - process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); - for (let index = 0; index < 2048; index += 1) { - process.stdout.write(JSON.stringify({ - method: "item/agentMessage/delta", - params: { - delta: "x", - itemId: "message-" + index, - threadId: "thread-child-" + (index % 2), - turnId: "turn-child-" + (index % 2) - } - }) + "\\n"); + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const request = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (request.method === "initialize") { + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); + } else if (request.method === "initialized") { + const notification = JSON.stringify({ + method: "warning", + params: { message: "x".repeat(${rejected ? "1" : "4096"}), threadId: null }, + }); + process.stdout.write( + Array.from({ length: ${String(messageCount)} }, () => notification).join("\\n") + "\\n", + () => writeFileSync(${JSON.stringify(join(directory, "burst-flushed"))}, "flushed"), + ); + } } }); setInterval(() => {}, 1000); @@ -1648,13 +1704,27 @@ setInterval(() => {}, 1000); try { await harness.client.start(); await firstNotification.promise; - await Bun.sleep(25); + expect(handled).toBe(1); expect(harness.protocolErrors).toEqual([]); + if (!rejected) { + expect(await Bun.file(join(harness.directory, "burst-flushed")).exists()).toBe(false); + } notificationGate.resolve(); - await harness.client.drainServerMessages(); - expect(handled).toBe(2048); - expect(harness.protocolErrors).toEqual([]); + const drain = harness.client.drainServerMessages(); + if (rejected) { + await expect(drain).rejects.toThrow("message queue limit exceeded"); + } else { + await drain; + expect(await Bun.file(join(harness.directory, "burst-flushed")).exists()).toBe(true); + } + expect(handled).toBe(Math.min(messageCount, 1_024)); + if (rejected) { + expect((await harness.protocolError).message).toContain("message queue limit exceeded"); + expect(harness.protocolErrors).toHaveLength(1); + } else { + expect(harness.protocolErrors).toEqual([]); + } } finally { notificationGate.resolve(); await harness.client.stop(); diff --git a/tests/openai-app-server-provider-fixtures.test.ts b/tests/openai-app-server-provider-fixtures.test.ts index 1e583e2..31f5bb4 100644 --- a/tests/openai-app-server-provider-fixtures.test.ts +++ b/tests/openai-app-server-provider-fixtures.test.ts @@ -121,10 +121,43 @@ function isIsoTimestamp(value: string): boolean { return value.endsWith("Z") && !Number.isNaN(Date.parse(value)); } -function normalizeBridgeValue(value: unknown, fieldName?: string): unknown { +function collectDriverIds(value: unknown, aliases: Map, fieldName?: string): void { if (typeof value === "string") { - if (isDriverId(value)) { - return ""; + const candidates = fieldName === "sourceEventId" ? value.split(":") : [value]; + for (const candidate of candidates) { + if (isDriverId(candidate) && !aliases.has(candidate)) { + aliases.set( + candidate, + aliases.size === 0 ? "" : ``, + ); + } + } + return; + } + + if (Array.isArray(value)) { + for (const entry of value) { + collectDriverIds(entry, aliases); + } + return; + } + + if (isRecord(value)) { + for (const [key, entry] of Object.entries(value)) { + collectDriverIds(entry, aliases, key); + } + } +} + +function normalizeBridgeValue( + value: unknown, + aliases: ReadonlyMap, + fieldName?: string, +): unknown { + if (typeof value === "string") { + const alias = aliases.get(value); + if (alias !== undefined) { + return alias; } if (fieldName !== undefined && fieldName.endsWith("At") && isIsoTimestamp(value)) { @@ -134,7 +167,7 @@ function normalizeBridgeValue(value: unknown, fieldName?: string): unknown { if (fieldName === "sourceEventId") { return value .split(":") - .map((part) => (isDriverId(part) ? "" : part)) + .map((part) => aliases.get(part) ?? part) .join(":"); } @@ -142,7 +175,7 @@ function normalizeBridgeValue(value: unknown, fieldName?: string): unknown { } if (Array.isArray(value)) { - return value.map((entry) => normalizeBridgeValue(entry)); + return value.map((entry) => normalizeBridgeValue(entry, aliases)); } if (!isRecord(value)) { @@ -150,26 +183,37 @@ function normalizeBridgeValue(value: unknown, fieldName?: string): unknown { } return Object.fromEntries( - Object.entries(value).map(([key, entry]) => [key, normalizeBridgeValue(entry, key)]), + Object.entries(value).map(([key, entry]) => [key, normalizeBridgeValue(entry, aliases, key)]), ); } -function normalizeBridgeEvent(event: DriverEventInput): Record { +function normalizeBridgeEvent( + event: DriverEventInput, + aliases: ReadonlyMap, +): Record { const eventRecord = event as unknown as Record; const normalized: Record = { kind: event.kind, - payload: normalizeBridgeValue(event.payload), + payload: normalizeBridgeValue(event.payload, aliases), }; for (const field of ["delivery", "native", "runId", "sourceEventId", "visibility"] as const) { if (eventRecord[field] !== undefined) { - normalized[field] = normalizeBridgeValue(eventRecord[field], field); + normalized[field] = normalizeBridgeValue(eventRecord[field], aliases, field); } } return normalized; } +function normalizeBridgeEvents(events: readonly DriverEventInput[]): Record[] { + const aliases = new Map(); + for (const event of events) { + collectDriverIds(event, aliases); + } + return events.map((event) => normalizeBridgeEvent(event, aliases)); +} + function createHarness(input: { sharedRootPath?: string } = {}) { const batches: EventBatch[] = []; const logger = createBufferedSinkLogger({ @@ -417,6 +461,27 @@ function createTurnFixture() { } describe("OpenAI app-server provider fixtures", () => { + test("normalizes generated IDs without collapsing their equivalence classes", () => { + const [normalized] = normalizeBridgeEvents([ + { + kind: "diagnostic.reported", + payload: { + first: DRIVER_TEST_IDS.runId, + repeated: DRIVER_TEST_IDS.runId, + second: DRIVER_TEST_IDS.secondRunId, + }, + sourceEventId: `test:${DRIVER_TEST_IDS.thirdRunId}`, + } as unknown as DriverEventInput, + ]); + + expect(normalized?.["payload"]).toEqual({ + first: "", + repeated: "", + second: "", + }); + expect(normalized?.["sourceEventId"]).toBe("test:"); + }); + test("matches the installed reasoning, MCP progress, and interactive request surface", () => { expect( parseNotificationParams("item/reasoning/summaryPartAdded", { @@ -1475,6 +1540,6 @@ describe("OpenAI app-server provider fixtures", () => { await assertTrackTurnFixture(bridge, fixture.trackTurnAfterNotifications); await logger.destroy(); - expect(events().map(normalizeBridgeEvent)).toEqual(fixture.expectedEvents); + expect(normalizeBridgeEvents(events())).toEqual(fixture.expectedEvents); }); }); diff --git a/tests/openai-app-server-startup.test.ts b/tests/openai-app-server-startup.test.ts index f472571..6c9980f 100644 --- a/tests/openai-app-server-startup.test.ts +++ b/tests/openai-app-server-startup.test.ts @@ -16,7 +16,6 @@ import { ACTIVE_TURN_CANCEL_GRACE_MS } from "../src/core/driver-command-dispatch import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; import { OpenAiAppServerClient } from "../src/runtimes/openai/app-server-client"; import { OpenAiAppServerDriverBackend } from "../src/runtimes/openai/app-server-driver-backend"; -import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; import { createCmaMemoryStore } from "../src/stores/memory"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; @@ -48,12 +47,20 @@ interface CancellationHarnessOptions { readonly failInitialThreadStart?: boolean; readonly holdCancellationRequest?: boolean; readonly holdRunCancellation?: boolean; + readonly holdMessageStart?: boolean; readonly holdTurnStartAfterItems?: boolean; readonly holdToolCompletion?: boolean; readonly restartResumeError?: string; readonly threadId?: string; readonly turnId?: string; readonly turnStartErrorMessage?: string; + readonly toolStartFollowup?: + | "error_response" + | "error_then_malformed" + | "error_then_success" + | "malformed" + | "terminal_response" + | "terminal_then_malformed"; readonly useCma?: boolean; } @@ -124,8 +131,7 @@ const sendInterrupted = (turnId) => process.stdout.write(JSON.stringify({ }, }, }) + "\\n"); -const sendToolLifecycle = (turnId, finishTurn, completeItem = true) => { - const item = { +const toolItem = { aggregatedOutput: "done", command: "printf done", commandActions: [], @@ -139,16 +145,19 @@ const sendToolLifecycle = (turnId, finishTurn, completeItem = true) => { source: "agent", status: "completed", type: "commandExecution", - }; - process.stdout.write(JSON.stringify({ +}; +const toolStartMessage = (turnId) => JSON.stringify({ method: "item/started", params: { - item: { ...item, aggregatedOutput: null, durationMs: null, exitCode: null, status: "inProgress" }, + item: { ...toolItem, aggregatedOutput: null, durationMs: null, exitCode: null, status: "inProgress" }, startedAtMs: 1, threadId: "fresh-thread", turnId, }, - }) + "\\n"); + }) + "\\n"; +const sendToolLifecycle = (turnId, finishTurn, completeItem = true) => { + const item = toolItem; + process.stdout.write(toolStartMessage(turnId)); if (completeItem) { process.stdout.write(JSON.stringify({ method: "item/completed", @@ -248,7 +257,14 @@ process.stdin.on("data", (chunk) => { const turnId = configuredTurnId ?? (launchNumber === 1 ? "turn-" + turnNumber : "turn-" + launchNumber + "-" + turnNumber); - const turnStartErrorMessage = ${JSON.stringify(cancellationOptions.turnStartErrorMessage ?? null)}; + const configuredToolStartFollowup = ${JSON.stringify(cancellationOptions.toolStartFollowup ?? null)}; + const toolStartFollowup = + request.method === "turn/start" && turnNumber === 1 ? configuredToolStartFollowup : null; + const configuredTurnStartErrorMessage = ${JSON.stringify(cancellationOptions.turnStartErrorMessage ?? null)}; + const turnStartErrorMessage = + configuredToolStartFollowup === "error_then_success" && turnNumber > 1 + ? null + : configuredTurnStartErrorMessage; const configuredResumeError = ${JSON.stringify(resumeErrorMessage)}; const restartResumeError = ${JSON.stringify(cancellationOptions.restartResumeError ?? null)}; const resumeError = @@ -259,8 +275,11 @@ process.stdin.on("data", (chunk) => { ? "no rollout found for thread id " + request.params.threadId : configuredResumeError; const terminalTurn = - (launchNumber > 1 || turnNumber > 1) && - !${JSON.stringify(cancellationOptions.emitToolCompletionOnTurnStart ?? false)}; + ["terminal_response", "terminal_then_malformed"].includes( + toolStartFollowup, + ) || + ((launchNumber > 1 || turnNumber > 1) && + !${JSON.stringify(cancellationOptions.emitToolCompletionOnTurnStart ?? false)}); const holdTurnStart = (${JSON.stringify(holdFirstTurnStartResponse)} || ${JSON.stringify(cancellationOptions.holdTurnStartAfterItems ?? false)}) && @@ -317,7 +336,26 @@ process.stdin.on("data", (chunk) => { sendDuplicateRequest(turnId); return; } - if (response !== null) process.stdout.write(JSON.stringify(response) + "\\n"); + if (request.method === "turn/start" && response !== null && toolStartFollowup === "malformed") { + process.stdout.write(JSON.stringify(response) + "\\n" + toolStartMessage(turnId) + "not-json\\n"); + } else if ( + request.method === "turn/start" && + response !== null && + (toolStartFollowup === "error_then_malformed" || + toolStartFollowup === "terminal_then_malformed") + ) { + process.stdout.write(toolStartMessage(turnId) + JSON.stringify(response) + "\\nnot-json\\n"); + } else if ( + request.method === "turn/start" && + response !== null && + (toolStartFollowup === "error_response" || + toolStartFollowup === "error_then_success" || + toolStartFollowup === "terminal_response") + ) { + process.stdout.write(toolStartMessage(turnId) + JSON.stringify(response) + "\\n"); + } else if (response !== null) { + process.stdout.write(JSON.stringify(response) + "\\n"); + } if (request.method === "turn/start" && duplicateRequestPhase === "active_turn") { setTimeout(() => sendDuplicateRequest(turnId), 5); } @@ -404,6 +442,8 @@ process.stdin.on("data", (chunk) => { let cancellationRequestFailures = 0; const cancellationRequestEntered = Promise.withResolvers(); const cancellationRequestGate = Promise.withResolvers(); + const messageStartEntered = Promise.withResolvers(); + const messageStartGate = Promise.withResolvers(); const runCancellationEntered = Promise.withResolvers(); const runCancellationGate = Promise.withResolvers(); const toolCompletionEntered = Promise.withResolvers(); @@ -421,6 +461,13 @@ process.stdin.on("data", (chunk) => { commandUpdate: async () => {}, currentRunId: () => activeRunId, pushEvents: async (input) => { + if ( + cancellationOptions.holdMessageStart === true && + input.events.some((event) => event.kind === "message.started") + ) { + messageStartEntered.resolve(); + await messageStartGate.promise; + } if ( holdTurnTiming && input.events.some((event) => event.sourceEventId === "openai.provider.turn_start:turn-1") @@ -520,9 +567,11 @@ process.stdin.on("data", (chunk) => { context, events, logger, + messageStartEntered: messageStartEntered.promise, payload, processLog, releaseCancellationRequest: () => cancellationRequestGate.resolve(), + releaseMessageStart: () => messageStartGate.resolve(), releaseRunCancellation: () => runCancellationGate.resolve(), releaseToolCompletion: () => toolCompletionGate.resolve(), releaseTurnTiming: () => turnTimingGate.resolve(), @@ -974,7 +1023,6 @@ describe("OpenAI app-server startup", () => { error: { message: reason }, status: "failed", }); - await Bun.sleep(25); expect( harness.events @@ -1071,6 +1119,141 @@ describe("OpenAI app-server startup", () => { } }); + test.each([ + ["terminal response", "terminal_response", "completed", "run.completed"], + [ + "terminal response followed by a malformed frame", + "terminal_then_malformed", + "completed", + "run.completed", + ], + ["error response", "error_response", "failed", "run.failed"], + [ + "error response followed by a malformed frame", + "error_then_malformed", + "failed", + "run.failed", + ], + ["malformed frame", "malformed", "failed", "run.failed"], + ] as const)( + "closes a durably started tool before a same-burst %s", + async (_label, toolStartFollowup, closureStatus, terminalKind) => { + const harness = await createCancellationHarness({ + holdMessageStart: true, + ...(toolStartFollowup === "error_response" || toolStartFollowup === "error_then_malformed" + ? { turnStartErrorMessage: "turn start rejected" } + : {}), + toolStartFollowup, + }); + + try { + await harness.backend.start(harness.context, new AbortController().signal); + const input = harness.backend.handleInput( + harness.context, + { text: "hello" }, + DRIVER_TEST_IDS.runId, + ); + void input.catch(() => {}); + await harness.messageStartEntered; + expect( + harness.events.some((event) => + ["run.cancelled", "run.completed", "run.failed"].includes(event.kind), + ), + ).toBe(false); + + harness.releaseMessageStart(); + if ( + toolStartFollowup !== "terminal_response" && + toolStartFollowup !== "terminal_then_malformed" + ) { + await expect(input).rejects.toThrow( + toolStartFollowup === "malformed" ? "stdout is not valid JSON" : "turn start rejected", + ); + } else { + await expect(input).resolves.toBeUndefined(); + } + + expect( + harness.events + .filter((event) => + [ + "item.completed", + "item.started", + "message.completed", + "message.failed", + "message.started", + "run.completed", + "run.failed", + "tool.call.updated", + ].includes(event.kind), + ) + .map((event) => [event.kind, eventPayloadStatus(event)]), + ).toEqual([ + ["message.started", undefined], + ["item.started", undefined], + ["tool.call.updated", "running"], + [ + toolStartFollowup === "terminal_response" || + toolStartFollowup === "terminal_then_malformed" + ? "message.completed" + : "message.failed", + undefined, + ], + ["tool.call.updated", closureStatus], + ["item.completed", closureStatus], + [terminalKind, undefined], + ]); + } finally { + harness.releaseMessageStart(); + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + await harness.logger.destroy(); + } + }, + 10_000, + ); + + test("does not carry failed turn-start item state into the next turn", async () => { + const harness = await createCancellationHarness({ + toolStartFollowup: "error_then_success", + turnStartErrorMessage: "turn start rejected", + }); + + try { + await harness.backend.start(harness.context, new AbortController().signal); + await expect( + harness.backend.handleInput(harness.context, { text: "first" }, DRIVER_TEST_IDS.runId), + ).rejects.toThrow("turn start rejected"); + await expect( + harness.backend.handleInput( + harness.context, + { text: "second" }, + DRIVER_TEST_IDS.secondRunId, + ), + ).resolves.toBeUndefined(); + + expect( + harness.events.filter((event) => event.kind === "item.completed").map(eventPayloadStatus), + ).toEqual(["failed"]); + expect( + harness.events + .filter((event) => event.kind === "tool.call.updated") + .map(eventPayloadStatus), + ).toEqual(["running", "failed"]); + expect( + harness.events + .filter((event) => ["run.completed", "run.failed"].includes(event.kind)) + .map((event) => event.kind), + ).toEqual(["run.failed", "run.completed"]); + } finally { + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + await harness.logger.destroy(); + } + }); + test("dispatcher cancellation closes a pending turn start and recovers on a new client", async () => { const harness = await createHarness( "no rollout found for thread id stale-thread", @@ -1308,21 +1491,7 @@ describe("OpenAI app-server startup", () => { "bounds a duplicate-request protocol failure during %s before terminal CMA delivery", async (duplicateRequestPhase) => { const harness = await createCancellationHarness({ duplicateRequestPhase }); - const lifecycleFailure = Promise.withResolvers(); - const lifecycleFail = spyOn(harness.context.lifecycle, "fail").mockImplementation((error) => - lifecycleFailure.resolve(error), - ); - const protocolFailureGate = Promise.withResolvers(); - const nativeFailActiveTurns = OpenAiAppServerEventBridge.prototype.failActiveTurns; - const failActiveTurns = - duplicateRequestPhase === "turn_start" - ? spyOn(OpenAiAppServerEventBridge.prototype, "failActiveTurns").mockImplementation( - async function (this: OpenAiAppServerEventBridge, context, error) { - await protocolFailureGate.promise; - return nativeFailActiveTurns.call(this, context, error); - }, - ) - : null; + const lifecycleFail = spyOn(harness.context.lifecycle, "fail").mockImplementation(() => {}); try { await harness.backend.start(harness.context, new AbortController().signal); @@ -1335,7 +1504,6 @@ describe("OpenAI app-server startup", () => { await expect(input).rejects.toThrow( "OpenAI provider error message was omitted because it contained 1100046 UTF-8 bytes.", ); - protocolFailureGate.resolve(); const terminal = harness.events.find((event) => event.kind === "run.failed"); expect(terminal).toMatchObject({ payload: { @@ -1351,25 +1519,8 @@ describe("OpenAI app-server startup", () => { }); expect(Buffer.byteLength(JSON.stringify(terminal), "utf8")).toBeLessThan(1_048_576); expect(harness.cmaEventTypes).toContain("session.error"); - if (duplicateRequestPhase === "turn_start") { - await expect( - settlePromiseWithTimeout(lifecycleFailure.promise, { - label: "bounded turn-start protocol lifecycle failure", - timeoutMs: 1_000, - }), - ).resolves.toMatchObject({ - status: "completed", - value: { - message: - "OpenAI provider error message was omitted because it contained 1100046 UTF-8 bytes.", - }, - }); - } else { - expect(lifecycleFail).not.toHaveBeenCalled(); - } + expect(lifecycleFail).not.toHaveBeenCalled(); } finally { - protocolFailureGate.resolve(); - failActiveTurns?.mockRestore(); lifecycleFail.mockRestore(); await harness.backend .stop(harness.context, "test complete", new AbortController().signal) @@ -1944,7 +2095,6 @@ describe("OpenAI app-server startup", () => { permissionGate.resolve(); await cancellation; await expect(input).rejects.toThrow("test.cancel"); - await Bun.sleep(25); const messages = (await readFile(harness.requestLog, "utf8")) .trim() diff --git a/tests/opencode-acp-contract.test.ts b/tests/opencode-acp-contract.test.ts index dd13bac..ae8e1f6 100644 --- a/tests/opencode-acp-contract.test.ts +++ b/tests/opencode-acp-contract.test.ts @@ -22,7 +22,7 @@ import { import { limitAcpInput } from "../src/runtimes/acp/acp-input-limit"; import { setupAcpSession } from "../src/runtimes/acp/acp-session-setup"; import { createBufferedSinkLogger } from "../src/observability"; -import { exposeNativeSkillAliases } from "../src/runtimes/skill-materialization"; +import { exposeNativeSkillAliases } from "../src/runtimes/skill-bootstrap"; import type { DriverStartInput } from "../src/protocol/start"; import { settlePromiseWithTimeout } from "../src/utils/async"; import { driverBootPayload, driverStartInput } from "./driver-boot-payload-fixture"; @@ -234,15 +234,20 @@ Check the diff.`, }); try { - await exposeNativeSkillAliases(execution, logger, [ - { - mountPath, - skillId: "skill-1", - skillMarkdownPath, - skillName: "review", - snapshotId: "snapshot-1", - }, - ]); + await exposeNativeSkillAliases( + execution, + logger, + [ + { + mountPath, + skillId: "skill-1", + skillMarkdownPath, + skillName: "review", + snapshotId: "snapshot-1", + }, + ], + new AbortController().signal, + ); const expectedSkillPath = join(await realpath(root), ".agents", "skills", "review", "SKILL.md"); expect(discoverOpenCodeSkills(root, homePath)).toContainEqual( @@ -252,7 +257,7 @@ Check the diff.`, }), ); - await exposeNativeSkillAliases(execution, logger, []); + await exposeNativeSkillAliases(execution, logger, [], new AbortController().signal); expect(discoverOpenCodeSkills(root, homePath).some((skill) => skill.name === "review")).toBe( false, diff --git a/tests/process-tree-watchdog.test.ts b/tests/process-tree-watchdog.test.ts index 05e76cb..09ca867 100644 --- a/tests/process-tree-watchdog.test.ts +++ b/tests/process-tree-watchdog.test.ts @@ -137,19 +137,37 @@ describe.skipIf(process.platform !== "linux")("Linux process-tree watchdog", () test.skipIf(process.getuid?.() === 0)( "keeps a newer unreadable process indeterminate", async () => { - const marker = createProcessTreeEnvironment(process.env).marker; + const processTree = createProcessTreeEnvironment(process.env); + const root = spawn("sleep", ["30"], { env: processTree.env }); + expect(root.pid).toBeDefined(); + processIds.add(root.pid!); + bindSpawnedProcess(root, process.platform, processTree); + root.kill("SIGKILL"); + await expectExited(root.pid!); const unreadable = await spawnNonDumpableProcess(); expect(() => readFileSync(`/proc/${unreadable.pid}/environ`)).toThrow(); - await expect(waitForLinuxProcessMarkerExit(marker, 100)).rejects.toThrow( + await expect(waitForLinuxProcessMarkerExit(processTree.marker, 100)).rejects.toThrow( "Supervised process tree did not exit", ); process.kill(unreadable.pid!, "SIGKILL"); await expectExited(unreadable.pid!); - await expect(waitForLinuxProcessMarkerExit(marker)).resolves.toBeUndefined(); + await expect(waitForLinuxProcessMarkerExit(processTree.marker)).resolves.toBeUndefined(); + releaseLinuxProcessMarker(processTree.marker); }, ); + test("does not reserve process state until a root is bound", async () => { + const processTrees = Array.from({ length: 64 }, () => + createProcessTreeEnvironment(process.env), + ); + + for (const processTree of processTrees) { + expect(spawnLinuxProcessTreeWatchdog(process.pid, processTree.marker)).toBeNull(); + await expect(waitForLinuxProcessMarkerExit(processTree.marker, 1)).resolves.toBeUndefined(); + } + }); + test("keeps a confirmed process after it clears the marker environment", async () => { const processTree = createProcessTreeEnvironment(process.env); const child = spawn("/bin/sh", ["-c", "kill -STOP $$; exec env -i /bin/sleep 30"], { @@ -159,6 +177,7 @@ describe.skipIf(process.platform !== "linux")("Linux process-tree watchdog", () }); expect(child.pid).toBeDefined(); processIds.add(child.pid!); + bindSpawnedProcess(child, process.platform, processTree); const stoppedDeadline = Date.now() + 3_000; let stopped = false; while (Date.now() < stoppedDeadline) { @@ -183,6 +202,7 @@ describe.skipIf(process.platform !== "linux")("Linux process-tree watchdog", () expect(signalLinuxProcessMarker(processTree.marker, "SIGKILL")).toBe(true); await waitForLinuxProcessMarkerExit(processTree.marker); await expectExited(child.pid!); + releaseLinuxProcessMarker(processTree.marker); }); test("rejects raw signals after the bound root identity changes", async () => { @@ -209,18 +229,17 @@ describe.skipIf(process.platform !== "linux")("Linux process-tree watchdog", () await expectExited(child.pid!); }); - test("does not trust a watchdog root without the marker", async () => { + test("does not supervise a root before marker ownership is bound", async () => { const marker = createProcessTreeEnvironment(process.env).marker; const child = spawn("sleep", ["30"]); expect(child.pid).toBeDefined(); processIds.add(child.pid!); const watchdog = spawnLinuxProcessTreeWatchdog(child.pid!, marker); - expect(watchdog).not.toBeNull(); + expect(watchdog).toBeNull(); expect(signalLinuxProcessMarker(marker, 0)).toBe(false); expect(isRunning(child.pid!)).toBe(true); child.kill("SIGKILL"); - await watchdog!.cleanup; await expectExited(child.pid!); }); diff --git a/tests/provider-registry.test.ts b/tests/provider-registry.test.ts index 2790306..60eb039 100644 --- a/tests/provider-registry.test.ts +++ b/tests/provider-registry.test.ts @@ -6,7 +6,6 @@ import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; import { AGENT_DRIVER_PROVIDER_REGISTRY, createAgentDriverProviderCapabilities, - createAgentDriverProviderRegistry, } from "../src/runtimes/provider-registry"; import { driverBootPayload } from "./driver-boot-payload-fixture"; @@ -84,34 +83,13 @@ describe("provider registry", () => { ); }); - test("declares provider host port requirements", () => { - expect( - AGENT_DRIVER_PROVIDER_REGISTRY.list().map((provider) => ({ - id: provider.id, - requiredHostPorts: provider.requiredHostPorts, - })), - ).toEqual([ - { - id: "openai-app-server", - requiredHostPorts: ["event_sink", "permission", "mcp", "skill"], - }, - { - id: "claude-agent-sdk", - requiredHostPorts: ["event_sink", "permission", "mcp", "skill"], - }, - { - id: "acp-fallback", - requiredHostPorts: ["event_sink", "permission", "mcp", "skill", "file", "host_integration"], - }, - ]); - }); - test("fails fast when no provider owns the transport", () => { - const registry = createAgentDriverProviderRegistry([]); - - expect(() => registry.createBackend(startInputFor("openai-app-server"))).toThrow( - "Unsupported runtime transport: openai-app-server.", - ); + expect(() => + AGENT_DRIVER_PROVIDER_REGISTRY.createBackend({ + ...startInputFor("openai-app-server"), + runtimeTransport: "unknown" as DriverRuntimeTransport, + }), + ).toThrow("Unsupported runtime transport: unknown."); }); test("fails fast when the start input runtime does not match the provider transport", () => { @@ -194,16 +172,4 @@ describe("provider registry", () => { ).toBe(input.runtime); }, ); - - test("rejects duplicate provider transports", () => { - const [provider] = AGENT_DRIVER_PROVIDER_REGISTRY.list(); - - if (!provider) { - throw new Error("Expected provider fixture."); - } - - expect(() => createAgentDriverProviderRegistry([provider, provider])).toThrow( - "Runtime transport openai-app-server is already registered by provider openai-app-server.", - ); - }); }); diff --git a/tests/public-api-consumer.ts b/tests/public-api-consumer.ts index 2104240..0c86e42 100644 --- a/tests/public-api-consumer.ts +++ b/tests/public-api-consumer.ts @@ -19,7 +19,3 @@ export interface PublicApiConsumer { readonly snapshot: SessionSnapshot; readonly startInput: DriverStartInput; } - -export function consumePublicApi(api: PublicApiConsumer): PublicApiConsumer { - return api; -} diff --git a/tests/public-api-exports.snapshot.json b/tests/public-api-exports.snapshot.json index a8e25cb..67b64e0 100644 --- a/tests/public-api-exports.snapshot.json +++ b/tests/public-api-exports.snapshot.json @@ -10,7 +10,6 @@ "AgentDriverEventSink", "AgentDriverFilePort", "AgentDriverHostIntegrationPort", - "AgentDriverHostPortName", "AgentDriverHostPorts", "AgentDriverKernel", "AgentDriverKernelCore", @@ -20,7 +19,6 @@ "AgentDriverMcpPort", "AgentDriverPermissionPort", "AgentDriverProviderDescriptor", - "AgentDriverProviderRegistry", "AgentDriverRuntimeEvent", "AgentDriverSkillPort", "CmaAgentRecord", @@ -55,6 +53,8 @@ "CmaSdkClientOptions", "CmaSdkError", "CmaSdkFetch", + "CmaSdkRequestOptions", + "CmaSdkStreamOptions", "CmaSessionEventDispatchRecord", "CmaSessionEventRecord", "CmaSessionRecord", @@ -121,13 +121,11 @@ "CmaUnsupportedFieldError", "DRIVER_ID_INPUT_PATTERN", "DRIVER_ID_PATTERN", - "OPENAI_DEFAULT_MODEL_ID", "SUPPORTED_DRIVER_NATIVE_RUNTIME_REF_KINDS", "SUPPORTED_DRIVER_RUNTIMES", "SUPPORTED_DRIVER_RUNTIME_TRANSPORTS", "createAgentDriverContext", "createAgentDriverProviderCapabilities", - "createAgentDriverProviderRegistry", "createCmaHttpHandler", "createCmaMemoryStore", "createCmaSdkClient", @@ -222,6 +220,8 @@ "CmaSdkClientOptions", "CmaSdkError", "CmaSdkFetch", + "CmaSdkRequestOptions", + "CmaSdkStreamOptions", "CmaSessionEventDispatchRecord" ], "values": ["CmaSdkError", "createCmaSdkClient"] @@ -518,7 +518,7 @@ ] }, "./paths": { - "types": ["SandboxFileBrowserPathPurpose"], + "types": [], "values": [ "SANDBOX_CACHE_PATH", "SANDBOX_MEMORY_PATH", diff --git a/tests/remote-http-mcp-executor.test.ts b/tests/remote-http-mcp-executor.test.ts index 1c5df0e..4f26998 100644 --- a/tests/remote-http-mcp-executor.test.ts +++ b/tests/remote-http-mcp-executor.test.ts @@ -3,7 +3,7 @@ import { expect, test } from "bun:test"; import type { CredentialId, McpServerId } from "../src/protocol/boot"; import type { DriverStartInput } from "../src/protocol/start"; import type { McpExecuteCommand } from "../src/runtime-command"; -import { executeRemoteHttpMcpCommand } from "../src/runtimes/mcp/remote-http-mcp-executor"; +import { prepareRemoteHttpMcpCommand } from "../src/runtimes/mcp/remote-http-mcp-executor"; import { driverStartInput } from "./driver-boot-payload-fixture"; const MCP_SERVER_ID = "01J00000000000000000000020" as McpServerId; @@ -47,10 +47,16 @@ function command(requestId: string): McpExecuteCommand { } async function execute(proxyUrl: string, requestId: string, signal = new AbortController().signal) { - return executeRemoteHttpMcpCommand(payload(proxyUrl), command(requestId), signal, { + await using prepared = await prepareRemoteHttpMcpCommand( + payload(proxyUrl), + command(requestId), + signal, + ); + const result = await prepared.execute({ effectId: `effect-${requestId}`, idempotencyKey: `key-${requestId}`, }); + return result; } test("classifies typed MCP HTTP failures by status", async () => { diff --git a/tests/skill-bootstrap.test.ts b/tests/skill-bootstrap.test.ts new file mode 100644 index 0000000..d755b4e --- /dev/null +++ b/tests/skill-bootstrap.test.ts @@ -0,0 +1,484 @@ +import { describe, expect, test } from "bun:test"; +import { + chmod, + mkdir, + mkdtemp, + open, + readFile, + readdir, + readlink, + rename, + rm, + stat, + symlink, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { FileHandle } from "node:fs/promises"; + +import type { DriverExecutionInput } from "../src/protocol/execution"; +import type { AgentDriverMaterializedSkill } from "../src/host-ports"; +import { closeFileHandles } from "../src/runtimes/atomic-file"; +import { + buildNativeRuntimeSystemPrompt, + writeNativeRuntimeSystemPrompt, + writeSkillBootstrapArtifacts, +} from "../src/runtimes/skill-bootstrap"; +import { bootPayload } from "./driver-runtime-boundary-fixtures"; + +function createExecution(root: string, systemPrompt: string): DriverExecutionInput { + return { + ...bootPayload.execution, + session: { + ...bootPayload.execution.session, + cwd: root, + homePath: root, + sharedRootPath: root, + }, + skillCatalog: [], + skills: [], + systemPrompt, + }; +} + +function createCatalogExecution(root: string): DriverExecutionInput { + return { + ...createExecution(root, ""), + skillCatalog: [ + { + frontmatter: { + author: null, + description: "Review code changes.", + version: null, + }, + mountPath: join(root, ".mosoo", "skill", "review"), + resolutionMode: "explicit", + skillId: "skill-1" as DriverExecutionInput["skillCatalog"][number]["skillId"], + skillName: "review", + }, + ], + }; +} + +function createMaterializedSkill(root: string): AgentDriverMaterializedSkill { + const mountPath = join(root, ".mosoo", "skill", "review"); + + return { + mountPath, + skillId: "skill-1", + skillMarkdownPath: join(mountPath, "SKILL.md"), + skillName: "review", + snapshotId: "snapshot-1", + }; +} + +async function interceptAtomicTemporarySync( + probePath: string, + directoryPath: string, + temporaryPrefix: string, + action: () => Promise, +): Promise<{ didRun: () => boolean; restore: () => void }> { + const probe = await open(probePath, "r"); + const prototype = Object.getPrototypeOf(probe) as { + sync(this: FileHandle): Promise; + }; + const nativeSync = prototype.sync; + let ran = false; + await probe.close(); + + prototype.sync = async function (this: FileHandle) { + await nativeSync.call(this); + if (ran) { + return; + } + let names: string[]; + try { + names = await readdir(directoryPath); + } catch { + // The watched directory may not exist until the writer creates it. + return; + } + if (names.some((name) => name.startsWith(temporaryPrefix))) { + ran = true; + await action(); + } + }; + + return { + didRun: () => ran, + restore: () => { + prototype.sync = nativeSync; + }, + }; +} + +describe("skill bootstrap", () => { + test("attempts every owned handle close when an earlier close fails", async () => { + const calls: string[] = []; + const first = { + async close() { + calls.push("first"); + throw new Error("first close failed"); + }, + } as unknown as FileHandle; + const second = { + async close() { + calls.push("second"); + }, + } as unknown as FileHandle; + + const failures = await closeFileHandles([first, second]); + + expect(calls).toEqual(["first", "second"]); + expect(failures).toHaveLength(1); + expect(failures[0]).toMatchObject({ message: "first close failed" }); + }); + + test("preserves bootstrap control sentences inside the profile prompt", () => { + const execution = createExecution( + "/workspace", + "Keep this literal.\nReply with exactly READY.", + ); + + expect(buildNativeRuntimeSystemPrompt(execution)).toBe( + [ + "Runtime context for this session.", + "", + "Agent profile prompt:", + "Keep this literal.", + "Reply with exactly READY.", + ].join("\n"), + ); + }); + + test("atomically replaces native instructions with mode 0600", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const path = join(root, "runtime-instructions.md"); + await writeFile(path, "old instructions\n", "utf8"); + await chmod(path, 0o644); + const oldFile = await open(path, "r"); + + try { + const execution = createExecution(root, "new instructions"); + + await expect( + writeNativeRuntimeSystemPrompt(execution, [], new AbortController().signal), + ).resolves.toBe(path); + expect(await readFile(path, "utf8")).toBe(`${buildNativeRuntimeSystemPrompt(execution)}\n`); + expect(await oldFile.readFile({ encoding: "utf8" })).toBe("old instructions\n"); + expect((await stat(path)).mode & 0o777).toBe(0o600); + expect(await readdir(root)).toEqual(["runtime-instructions.md"]); + } finally { + await oldFile.close(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("does not replace native instructions after startup cancellation", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const path = join(root, "runtime-instructions.md"); + await writeFile(path, "previous instructions\n", "utf8"); + const controller = new AbortController(); + controller.abort(new Error("startup cancelled")); + + try { + await expect( + writeNativeRuntimeSystemPrompt( + createExecution(root, "new instructions"), + [], + controller.signal, + ), + ).rejects.toThrow("startup cancelled"); + await expect(readFile(path, "utf8")).resolves.toBe("previous instructions\n"); + await expect(readdir(root)).resolves.toEqual(["runtime-instructions.md"]); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + test("atomically replaces a native instructions symlink without changing its target", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-outside-")); + const path = join(root, "runtime-instructions.md"); + const target = join(outsideRoot, "target.md"); + await writeFile(target, "outside contents\n", "utf8"); + await symlink(target, path, "file"); + + try { + await expect( + writeNativeRuntimeSystemPrompt( + createExecution(root, "new instructions"), + [], + new AbortController().signal, + ), + ).resolves.toBe(path); + expect(await readFile(target, "utf8")).toBe("outside contents\n"); + await expect(readlink(path)).rejects.toThrow(); + expect((await stat(path)).mode & 0o777).toBe(0o600); + expect(await readdir(root)).toEqual(["runtime-instructions.md"]); + } finally { + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("rejects a native instructions symlink ancestor without writing through it", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-outside-")); + const homePath = join(root, "linked-home", "runtime"); + await symlink(outsideRoot, join(root, "linked-home"), "dir"); + const baseExecution = createExecution(root, "new instructions"); + const execution = { + ...baseExecution, + session: { ...baseExecution.session, homePath }, + }; + + try { + await expect( + writeNativeRuntimeSystemPrompt(execution, [], new AbortController().signal), + ).rejects.toThrow("must be a real directory"); + await expect(readdir(outsideRoot)).resolves.toEqual([]); + } finally { + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("rejects a native home directory replaced while instructions are written", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-outside-")); + const homePath = join(root, "home"); + const detachedHome = join(root, "detached-home"); + await mkdir(homePath); + await writeFile(join(outsideRoot, "runtime-instructions.md"), "ATTACKER\n", "utf8"); + const baseExecution = createExecution(root, "x".repeat(8 * 1024 * 1024)); + const execution = { + ...baseExecution, + session: { ...baseExecution.session, homePath }, + }; + const interception = await interceptAtomicTemporarySync( + root, + homePath, + ".runtime-instructions.md.", + async () => { + await rename(homePath, detachedHome); + await symlink(outsideRoot, homePath, "dir"); + }, + ); + + try { + await expect( + writeNativeRuntimeSystemPrompt(execution, [], new AbortController().signal), + ).rejects.toThrow("Runtime home changed while managed files were being written"); + expect(interception.didRun()).toBe(true); + await expect(readFile(join(outsideRoot, "runtime-instructions.md"), "utf8")).resolves.toBe( + "ATTACKER\n", + ); + await expect(readFile(join(detachedHome, "runtime-instructions.md"), "utf8")).resolves.toBe( + `${buildNativeRuntimeSystemPrompt(execution)}\n`, + ); + } finally { + interception.restore(); + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("concurrent native instruction writers never share a temporary file", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const first = createExecution(root, `first-${"a".repeat(2 * 1024 * 1024)}`); + const second = createExecution(root, `second-${"b".repeat(2 * 1024 * 1024)}`); + + try { + await Promise.all([ + writeNativeRuntimeSystemPrompt(first, [], new AbortController().signal), + writeNativeRuntimeSystemPrompt(second, [], new AbortController().signal), + ]); + + const contents = await readFile(join(root, "runtime-instructions.md"), "utf8"); + expect([ + `${buildNativeRuntimeSystemPrompt(first)}\n`, + `${buildNativeRuntimeSystemPrompt(second)}\n`, + ]).toContain(contents); + expect(await readdir(root)).toEqual(["runtime-instructions.md"]); + expect((await stat(join(root, "runtime-instructions.md"))).mode & 0o777).toBe(0o600); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + test("atomically replaces catalog leaf symlinks without changing their targets", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-outside-")); + const catalogRoot = join(root, ".mosoo", "skills"); + const manifestPath = join(catalogRoot, "manifest.json"); + const readmePath = join(catalogRoot, "README.md"); + const manifestTarget = join(outsideRoot, "manifest-target.json"); + const readmeTarget = join(outsideRoot, "readme-target.md"); + await mkdir(catalogRoot, { recursive: true }); + await writeFile(manifestTarget, "outside manifest", "utf8"); + await writeFile(readmeTarget, "outside readme", "utf8"); + await symlink(manifestTarget, manifestPath, "file"); + await symlink(readmeTarget, readmePath, "file"); + + try { + await expect( + writeSkillBootstrapArtifacts( + createCatalogExecution(root), + [createMaterializedSkill(root)], + new AbortController().signal, + ), + ).resolves.toEqual({ manifestPath, readmePath }); + expect(await readFile(manifestTarget, "utf8")).toBe("outside manifest"); + expect(await readFile(readmeTarget, "utf8")).toBe("outside readme"); + await expect(readlink(manifestPath)).rejects.toThrow(); + await expect(readlink(readmePath)).rejects.toThrow(); + } finally { + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("rejects a catalog directory replaced while bootstrap files are written", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-outside-")); + const catalogRoot = join(root, ".mosoo", "skills"); + const detachedCatalog = join(root, ".mosoo", "detached-skills"); + await mkdir(catalogRoot, { recursive: true }); + await writeFile(join(outsideRoot, "manifest.json"), "ATTACKER", "utf8"); + await writeFile(join(outsideRoot, "README.md"), "ATTACKER", "utf8"); + const execution = createCatalogExecution(root); + execution.skillCatalog[0] = { + ...execution.skillCatalog[0]!, + frontmatter: { + ...execution.skillCatalog[0]!.frontmatter, + description: "x".repeat(8 * 1024 * 1024), + }, + }; + const interception = await interceptAtomicTemporarySync( + root, + catalogRoot, + ".manifest.json.", + async () => { + await rename(catalogRoot, detachedCatalog); + await symlink(outsideRoot, catalogRoot, "dir"); + }, + ); + + try { + await expect( + writeSkillBootstrapArtifacts( + execution, + [createMaterializedSkill(root)], + new AbortController().signal, + ), + ).rejects.toThrow("catalog root changed while managed files were being written"); + expect(interception.didRun()).toBe(true); + await expect(readFile(join(outsideRoot, "manifest.json"), "utf8")).resolves.toBe("ATTACKER"); + await expect(readFile(join(outsideRoot, "README.md"), "utf8")).resolves.toBe("ATTACKER"); + await expect(readFile(join(detachedCatalog, "manifest.json"), "utf8")).resolves.toContain( + '"skillName": "review"', + ); + } finally { + interception.restore(); + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("rejects a catalog symlink ancestor without writing outside the session", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-outside-")); + await mkdir(join(root, ".mosoo")); + await symlink(outsideRoot, join(root, ".mosoo", "skills"), "dir"); + + try { + await expect( + writeSkillBootstrapArtifacts( + createCatalogExecution(root), + [createMaterializedSkill(root)], + new AbortController().signal, + ), + ).rejects.toThrow("must be a real directory"); + expect(await readdir(outsideRoot)).toEqual([]); + } finally { + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("does not expose a catalog entry that was not materialized", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const execution = createCatalogExecution(root); + execution.skillCatalog[0] = { + ...execution.skillCatalog[0]!, + mountPath: join(root, ".mosoo", "skill", "other"), + skillName: "other", + }; + + try { + await expect( + writeSkillBootstrapArtifacts( + execution, + [createMaterializedSkill(root)], + new AbortController().signal, + ), + ).rejects.toThrow("does not match materialized skill"); + await expect(readdir(root)).resolves.toEqual([]); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + test("does not write native instructions for an unmaterialized catalog entry", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const execution = createCatalogExecution(root); + + try { + await expect( + writeNativeRuntimeSystemPrompt(execution, [], new AbortController().signal), + ).rejects.toThrow("does not match materialized skill"); + await expect(readFile(join(root, "runtime-instructions.md"), "utf8")).rejects.toThrow(); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + test("removes stale managed artifacts when the runtime context becomes empty", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-bootstrap-")); + const catalogRoot = join(root, ".mosoo", "skills"); + const nativeInstructionsPath = join(root, "runtime-instructions.md"); + const nativeTemporaryPath = join( + root, + ".runtime-instructions.md.11111111-1111-4111-8111-111111111111.tmp", + ); + await mkdir(catalogRoot, { recursive: true }); + await writeFile(join(catalogRoot, "manifest.json"), "stale manifest", "utf8"); + await writeFile(join(catalogRoot, "README.md"), "stale readme", "utf8"); + await writeFile( + join(catalogRoot, ".manifest.json.22222222-2222-4222-8222-222222222222.tmp"), + "partial manifest", + "utf8", + ); + await writeFile( + join(catalogRoot, ".README.md.33333333-3333-4333-8333-333333333333.tmp"), + "partial readme", + "utf8", + ); + await writeFile(nativeInstructionsPath, "stale instructions", "utf8"); + await writeFile(nativeTemporaryPath, "partial instructions", "utf8"); + const execution = createExecution(root, ""); + const signal = new AbortController().signal; + + try { + await expect(writeSkillBootstrapArtifacts(execution, [], signal)).resolves.toBeNull(); + await expect(writeNativeRuntimeSystemPrompt(execution, [], signal)).resolves.toBeNull(); + await expect(readdir(catalogRoot)).resolves.toEqual([]); + await expect(readFile(nativeInstructionsPath, "utf8")).rejects.toThrow(); + await expect(readFile(nativeTemporaryPath, "utf8")).rejects.toThrow(); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); +}); diff --git a/tests/skill-materialization.test.ts b/tests/skill-materialization.test.ts index 0346844..5abb31a 100644 --- a/tests/skill-materialization.test.ts +++ b/tests/skill-materialization.test.ts @@ -1,28 +1,30 @@ import { describe, expect, test } from "bun:test"; import { Buffer } from "node:buffer"; import { createHash } from "node:crypto"; +import { existsSync } from "node:fs"; import { mkdir, mkdtemp, + open, readFile, readdir, readlink, + rename, rm, symlink, writeFile, } from "node:fs/promises"; +import type { FileHandle } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { zipSync as createZipArchive } from "fflate"; import type { AgentDriverMaterializedSkill } from "../src/host-ports"; import { createBufferedSinkLogger } from "../src/observability"; import type { DriverResolvedSkill } from "../src/protocol/boot"; import type { DriverExecutionInput } from "../src/protocol/execution"; -import { - exposeNativeSkillAliases, - materializeResolvedSkills, -} from "../src/runtimes/skill-materialization"; +import { exposeNativeSkillAliases } from "../src/runtimes/skill-bootstrap"; +import { materializeResolvedSkills } from "../src/runtimes/skill-materialization"; import { bootPayload } from "./driver-runtime-boundary-fixtures"; const textEncoder = new TextEncoder(); @@ -35,7 +37,12 @@ function toDataUrl(bytes: Uint8Array): string { return `data:application/zip;base64,${Buffer.from(bytes).toString("base64")}`; } -function createExecution(root: string, skill: DriverResolvedSkill): DriverExecutionInput { +function createExecution( + root: string, + skills: DriverResolvedSkill | DriverResolvedSkill[], +): DriverExecutionInput { + const resolvedSkills = Array.isArray(skills) ? skills : [skills]; + return { ...bootPayload.execution, session: { @@ -43,12 +50,26 @@ function createExecution(root: string, skill: DriverResolvedSkill): DriverExecut cwd: root, sharedRootPath: root, }, - skillCatalog: [], - skills: [skill], + skillCatalog: resolvedSkills.map((skill) => ({ + frontmatter: { + author: null, + description: null, + version: null, + }, + mountPath: skill.mountPath, + resolutionMode: skill.resolutionMode, + skillId: skill.skillId, + skillName: skill.skillName, + })), + skills: resolvedSkills, }; } -function createSkill(root: string, archive: Uint8Array): DriverResolvedSkill { +function createSkill( + root: string, + archive: Uint8Array, + overrides: Partial = {}, +): DriverResolvedSkill { return { archiveFormat: "zip", blobSha256: sha256(archive), @@ -61,9 +82,23 @@ function createSkill(root: string, archive: Uint8Array): DriverResolvedSkill { skillName: "review", snapshotId: "snapshot-1" as DriverResolvedSkill["snapshotId"], warningCode: null, + ...overrides, }; } +function materialize(execution: DriverExecutionInput, logger: ReturnType) { + return materializeResolvedSkills(execution, logger, new AbortController().signal); +} + +function exposeAliases( + execution: DriverExecutionInput, + logger: ReturnType, + skills: readonly AgentDriverMaterializedSkill[], + signal = new AbortController().signal, +) { + return exposeNativeSkillAliases(execution, logger, skills, signal); +} + function createTestLogger() { return createBufferedSinkLogger({ level: "debug", @@ -76,6 +111,20 @@ function createMarkdownSkillEntries(markdown: string) { return { "SKILL.md": textEncoder.encode(markdown) }; } +async function createActiveTransaction(root: string, committed = false) { + const activeRoot = join(root, ".mosoo", ".skill-transactions", "active"); + const newRoot = join(activeRoot, "new"); + const oldRoot = join(activeRoot, "old"); + await mkdir(activeRoot, { recursive: true }); + if (committed) { + await mkdir(oldRoot); + await writeFile(join(activeRoot, "COMMITTED"), ""); + } else { + await mkdir(newRoot); + } + return { activeRoot, newRoot, oldRoot }; +} + describe("skill materialization", () => { test("extracts a resolved skill under the session skill root", async () => { const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); @@ -91,9 +140,9 @@ Check the diff.`), const skill = createSkill(root, archive); try { - const [materialized] = await materializeResolvedSkills(createExecution(root, skill), logger); + const [materializedSkill] = await materialize(createExecution(root, skill), logger); - expect(materialized).toEqual({ + expect(materializedSkill).toEqual({ mountPath: skill.mountPath, skillId: "skill-1", skillMarkdownPath: join(skill.mountPath, "SKILL.md"), @@ -126,7 +175,7 @@ Check the diff.`), }; try { - await expect(materializeResolvedSkills(createExecution(root, skill), logger)).rejects.toThrow( + await expect(materialize(createExecution(root, skill), logger)).rejects.toThrow( "outside the allowed root", ); } finally { @@ -135,24 +184,833 @@ Check the diff.`), } }); - test("fails malformed packages before reporting materialization success", async () => { + test("rejects duplicate resolved mounts before touching either skill", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const archive = createZipArchive(createMarkdownSkillEntries("canonical")); + const first = createSkill(root, archive); + const duplicate = createSkill(root, archive, { + mountPath: join(root, ".mosoo", "skill", "unused", "..", "review"), + skillId: "skill-2" as DriverResolvedSkill["skillId"], + skillName: "duplicate", + snapshotId: "snapshot-2" as DriverResolvedSkill["snapshotId"], + }); + await mkdir(first.mountPath, { recursive: true }); + await writeFile(join(first.mountPath, "KEEP"), "untouched", "utf8"); + + try { + await expect(materialize(createExecution(root, [first, duplicate]), logger)).rejects.toThrow( + "duplicate mount path", + ); + await expect(readFile(join(first.mountPath, "KEEP"), "utf8")).resolves.toBe("untouched"); + await expect(readdir(join(root, ".mosoo"))).resolves.toEqual(["skill"]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("rejects duplicate resolved skill names before downloading either skill", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const archive = createZipArchive(createMarkdownSkillEntries("canonical")); + const first = createSkill(root, archive); + const duplicate = createSkill(root, archive, { + mountPath: join(root, ".mosoo", "skill", "second"), + skillId: "skill-2" as DriverResolvedSkill["skillId"], + snapshotId: "snapshot-2" as DriverResolvedSkill["snapshotId"], + }); + + try { + await expect(materialize(createExecution(root, [first, duplicate]), logger)).rejects.toThrow( + "duplicate skill name", + ); + await expect(readdir(root)).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("rejects a catalog that advertises a different active skill", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const archive = createZipArchive(createMarkdownSkillEntries("canonical")); + const skill = createSkill(root, archive); + const execution = createExecution(root, skill); + execution.skillCatalog[0] = { + ...execution.skillCatalog[0]!, + mountPath: join(root, ".mosoo", "skill", "other"), + skillName: "other", + }; + + try { + await expect(materialize(execution, logger)).rejects.toThrow("does not match resolved skill"); + await expect(readdir(root)).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("rejects a symbolic-link mount ancestor without touching its target", async () => { const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-outside-")); + const logger = createTestLogger(); + const archive = createZipArchive(createMarkdownSkillEntries("new contents")); + const skill = createSkill(root, archive); + const outsideMount = join(outsideRoot, "review"); + await mkdir(join(root, ".mosoo")); + await mkdir(outsideMount, { recursive: true }); + await writeFile(join(outsideMount, "KEEP"), "outside", "utf8"); + await symlink(outsideRoot, join(root, ".mosoo", "skill"), "dir"); + + try { + await expect(materialize(createExecution(root, skill), logger)).rejects.toThrow( + "Skill mount root must be a real directory", + ); + await expect(readFile(join(outsideMount, "KEEP"), "utf8")).resolves.toBe("outside"); + await expect(readlink(join(root, ".mosoo", "skill"))).resolves.toBe(outsideRoot); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("rejects a symbolic-link mount leaf without touching its target", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-outside-")); + const logger = createTestLogger(); + const archive = createZipArchive(createMarkdownSkillEntries("new contents")); + const skill = createSkill(root, archive); + await mkdir(dirname(skill.mountPath), { recursive: true }); + await writeFile(join(outsideRoot, "KEEP"), "outside", "utf8"); + await symlink(outsideRoot, skill.mountPath, "dir"); + + try { + await expect(materialize(createExecution(root, skill), logger)).rejects.toThrow( + "must be a real directory or absent", + ); + await expect(readFile(join(outsideRoot, "KEEP"), "utf8")).resolves.toBe("outside"); + await expect(readlink(skill.mountPath)).resolves.toBe(outsideRoot); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("an empty catalog fails closed for a symbolic-link live root", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-outside-")); + const logger = createTestLogger(); + await mkdir(join(root, ".mosoo")); + await writeFile(join(outsideRoot, "KEEP"), "outside", "utf8"); + await symlink(outsideRoot, join(root, ".mosoo", "skill"), "dir"); + + try { + await expect(materialize(createExecution(root, []), logger)).rejects.toThrow( + "must be a real directory", + ); + await expect(readFile(join(outsideRoot, "KEEP"), "utf8")).resolves.toBe("outside"); + await expect(readlink(join(root, ".mosoo", "skill"))).resolves.toBe(outsideRoot); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("an empty catalog fails closed for a non-directory live root", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const mountRoot = join(root, ".mosoo", "skill"); + await mkdir(join(root, ".mosoo")); + await writeFile(mountRoot, "user-controlled", "utf8"); + + try { + await expect(materialize(createExecution(root, []), logger)).rejects.toThrow( + "must be a real directory", + ); + await expect(readFile(mountRoot, "utf8")).resolves.toBe("user-controlled"); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("recovery does not accept a symbolic-link untouched live root", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-outside-")); + const logger = createTestLogger(); + await mkdir(join(root, ".mosoo")); + await writeFile(join(outsideRoot, "KEEP"), "outside", "utf8"); + await symlink(outsideRoot, join(root, ".mosoo", "skill"), "dir"); + await createActiveTransaction(root); + + try { + await expect(materialize(createExecution(root, []), logger)).rejects.toThrow( + "must be a real directory", + ); + await expect(readFile(join(outsideRoot, "KEEP"), "utf8")).resolves.toBe("outside"); + await expect(readlink(join(root, ".mosoo", "skill"))).resolves.toBe(outsideRoot); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("does not follow a mount ancestor replaced after admission", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-outside-")); + const logger = createTestLogger(); + const archive = createZipArchive(createMarkdownSkillEntries("new contents")); + const skill = createSkill(root, archive, { + downloadUrl: "https://skills.test/raced-ancestor.zip", + }); + const requestStarted = Promise.withResolvers(); + const releaseResponse = Promise.withResolvers(); + const nativeFetch = globalThis.fetch; + globalThis.fetch = (async () => { + requestStarted.resolve(); + await releaseResponse.promise; + return new Response(archive); + }) as unknown as typeof fetch; + await mkdir(skill.mountPath, { recursive: true }); + await writeFile(join(skill.mountPath, "SKILL.md"), "previous contents", "utf8"); + + try { + const result = materialize(createExecution(root, skill), logger); + const rejection = result.then( + () => null, + (error: unknown) => error, + ); + await requestStarted.promise; + const mountRoot = dirname(skill.mountPath); + const detachedMountRoot = join(root, ".mosoo", "detached-skill"); + await rename(mountRoot, detachedMountRoot); + await symlink(outsideRoot, mountRoot, "dir"); + releaseResponse.resolve(); + + await expect(rejection).resolves.toMatchObject({ + message: expect.stringContaining("must be a real directory"), + }); + await expect(readdir(outsideRoot)).resolves.toEqual([]); + await expect(readFile(join(detachedMountRoot, "review", "SKILL.md"), "utf8")).resolves.toBe( + "previous contents", + ); + } finally { + globalThis.fetch = nativeFetch; + releaseResponse.resolve(); + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("does not follow a staged archive ancestor replaced during extraction", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-outside-")); const logger = createTestLogger(); const archive = createZipArchive({ + ...createMarkdownSkillEntries("canonical"), + "nested/first.txt": textEncoder.encode("first"), + "nested/second.txt": textEncoder.encode("second"), + }); + const skill = createSkill(root, archive); + const probe = await open(root, "r"); + const handlePrototype = Object.getPrototypeOf(probe) as { + sync(this: FileHandle): Promise; + }; + const nativeSync = handlePrototype.sync; + let swapped = false; + await probe.close(); + await writeFile(join(outsideRoot, "KEEP"), "outside", "utf8"); + + try { + handlePrototype.sync = async function (this: FileHandle) { + await nativeSync.call(this); + if (swapped) { + return; + } + const transactionRoot = join(root, ".mosoo", ".skill-transactions"); + let ownerNames: string[]; + try { + ownerNames = await readdir(transactionRoot); + } catch { + return; + } + const ownerName = ownerNames.find((name) => name.startsWith("stage-")); + if (ownerName === undefined) { + return; + } + const reviewRoot = join(transactionRoot, ownerName, "new", "review"); + const nestedPath = join(reviewRoot, "nested"); + if (!existsSync(join(nestedPath, "first.txt"))) { + return; + } + + swapped = true; + await rename(nestedPath, join(reviewRoot, "detached-nested")); + await symlink(outsideRoot, nestedPath, "dir"); + }; + + await expect(materialize(createExecution(root, skill), logger)).rejects.toThrow( + "must be a real directory", + ); + expect(swapped).toBe(true); + await expect(readFile(join(outsideRoot, "KEEP"), "utf8")).resolves.toBe("outside"); + await expect(readFile(join(outsideRoot, "second.txt"), "utf8")).rejects.toThrow(); + await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).rejects.toThrow(); + } finally { + handlePrototype.sync = nativeSync; + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("preserves the previous tree when a replacement package is malformed", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const validArchive = createZipArchive(createMarkdownSkillEntries("previous contents")); + const malformedArchive = createZipArchive({ "references/README.md": textEncoder.encode("missing skill markdown"), }); + const validSkill = createSkill(root, validArchive); + const malformedSkill = createSkill(root, malformedArchive, { + snapshotId: "snapshot-2" as DriverResolvedSkill["snapshotId"], + }); + + try { + await materialize(createExecution(root, validSkill), logger); + await expect(materialize(createExecution(root, malformedSkill), logger)).rejects.toThrow( + "does not contain SKILL.md", + ); + await expect(readFile(join(validSkill.mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "previous contents", + ); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("does not trust cache metadata in the agent-writable skill tree", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const archive = createZipArchive(createMarkdownSkillEntries("canonical contents")); + const skill = createSkill(root, archive); + + try { + await materialize(createExecution(root, skill), logger); + await writeFile(join(skill.mountPath, "SKILL.md"), "tampered", "utf8"); + await writeFile( + join(skill.mountPath, ".mosoo-skill-cache.json"), + JSON.stringify({ blobSha256: skill.blobSha256, snapshotId: skill.snapshotId }), + "utf8", + ); + + await materialize(createExecution(root, skill), logger); + + await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "canonical contents", + ); + await expect( + readFile(join(skill.mountPath, ".mosoo-skill-cache.json"), "utf8"), + ).rejects.toThrow(); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("quarantine cleanup unlinks old symlinks without traversing their targets", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const outsideRoot = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-outside-")); + const logger = createTestLogger(); + const archive = createZipArchive(createMarkdownSkillEntries("replacement contents")); const skill = createSkill(root, archive); + await mkdir(skill.mountPath, { recursive: true }); + await writeFile(join(skill.mountPath, "SKILL.md"), "previous contents", "utf8"); + await writeFile(join(outsideRoot, "KEEP"), "outside", "utf8"); + await symlink(outsideRoot, join(skill.mountPath, "escape"), "dir"); try { - await expect(materializeResolvedSkills(createExecution(root, skill), logger)).rejects.toThrow( + await materialize(createExecution(root, skill), logger); + + await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "replacement contents", + ); + await expect(readFile(join(outsideRoot, "KEEP"), "utf8")).resolves.toBe("outside"); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + await rm(outsideRoot, { force: true, recursive: true }); + } + }); + + test("quarantines managed mounts that are absent from an empty catalog", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const skill = createSkill( + root, + createZipArchive(createMarkdownSkillEntries("previous contents")), + ); + + try { + await materialize(createExecution(root, skill), logger); + await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); + + await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).rejects.toThrow(); + await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("does not create managed roots for an already empty session", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const execution = createExecution(root, []); + + try { + await expect(materialize(execution, logger)).resolves.toEqual([]); + await expect(exposeAliases(execution, logger, [])).resolves.toEqual([]); + await expect(readdir(root)).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("an empty catalog supersedes an older download for the whole mount root", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const archive = createZipArchive(createMarkdownSkillEntries("obsolete contents")); + const requestStarted = Promise.withResolvers(); + const releaseResponse = Promise.withResolvers(); + const nativeFetch = globalThis.fetch; + globalThis.fetch = (async (input: Parameters[0]) => { + if (String(input) !== "https://skills.test/obsolete.zip") { + return nativeFetch(input); + } + + requestStarted.resolve(); + await releaseResponse.promise; + return new Response(archive); + }) as typeof fetch; + const oldSkill = createSkill(root, archive, { + downloadUrl: "https://skills.test/obsolete.zip", + }); + + try { + const oldResult = materialize(createExecution(root, oldSkill), logger).then( + () => null, + (error: unknown) => error, + ); + await requestStarted.promise; + + await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); + releaseResponse.resolve(); + + await expect(oldResult).resolves.toMatchObject({ + message: "Skill materialization was superseded by a newer generation.", + }); + await expect(readFile(join(oldSkill.mountPath, "SKILL.md"), "utf8")).rejects.toThrow(); + } finally { + releaseResponse.resolve(); + globalThis.fetch = nativeFetch; + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("cancellation after old mounts move still rolls back the whole catalog", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const skill = createSkill( + root, + createZipArchive(createMarkdownSkillEntries("previous contents")), + ); + const controller = new AbortController(); + const probe = await open(root, "r"); + const handlePrototype = Object.getPrototypeOf(probe) as { + sync(this: FileHandle): Promise; + }; + const nativeSync = handlePrototype.sync; + await probe.close(); + + try { + await materialize(createExecution(root, skill), logger); + handlePrototype.sync = async function (this: FileHandle) { + await nativeSync.call(this); + if (!existsSync(skill.mountPath)) { + controller.abort(new Error("cancel after backup")); + } + }; + + await expect( + materializeResolvedSkills(createExecution(root, []), logger, controller.signal), + ).rejects.toThrow("cancel after backup"); + await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "previous contents", + ); + await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); + } finally { + handlePrototype.sync = nativeSync; + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("recovers a bounded orphan transaction owner", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const transactionRoot = join(root, ".mosoo", ".skill-transactions"); + const transactionId = "44444444-4444-4444-8444-444444444444"; + const stagePath = join(transactionRoot, `stage-${transactionId}`); + await mkdir(join(root, ".mosoo", "skill"), { recursive: true }); + await mkdir(stagePath, { recursive: true }); + await writeFile(join(stagePath, "partial"), "orphan", "utf8"); + + try { + await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); + await expect(readdir(transactionRoot)).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("makes bounded progress cleaning a deeply nested orphan owner", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const transactionRoot = join(root, ".mosoo", ".skill-transactions"); + const stagePath = join(transactionRoot, "stage-55555555-5555-4555-8555-555555555555"); + const deepPath = join( + stagePath, + ...Array.from({ length: 80 }, (_, index) => `level-${String(index)}`), + ); + await mkdir(join(root, ".mosoo", "skill"), { recursive: true }); + await mkdir(deepPath, { recursive: true }); + await writeFile(join(deepPath, "leaf"), "orphan", "utf8"); + + try { + await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); + expect(await readdir(transactionRoot)).toHaveLength(1); + await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); + await expect(readdir(transactionRoot)).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("fsyncs the shared root after creating the managed .mosoo entry", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const skill = createSkill( + root, + createZipArchive(createMarkdownSkillEntries("durable contents")), + ); + const probe = await open(root, "r"); + const rootStats = await probe.stat(); + const handlePrototype = Object.getPrototypeOf(probe) as { + sync(this: FileHandle): Promise; + }; + const nativeSync = handlePrototype.sync; + let sharedRootSynced = false; + await probe.close(); + + try { + handlePrototype.sync = async function (this: FileHandle) { + const stats = await this.stat(); + if (stats.dev === rootStats.dev && stats.ino === rootStats.ino) { + sharedRootSynced = true; + } + await nativeSync.call(this); + }; + await materialize(createExecution(root, skill), logger); + expect(sharedRootSynced).toBe(true); + } finally { + handlePrototype.sync = nativeSync; + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("restores an interrupted whole-catalog swap before its commit marker", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const mountPath = join(root, ".mosoo", "skill", "review"); + const malformedSkill = createSkill( + root, + createZipArchive({ "README.md": textEncoder.encode("missing skill markdown") }), + ); + await mkdir(mountPath, { recursive: true }); + await writeFile(join(mountPath, "SKILL.md"), "previous contents", "utf8"); + const { newRoot, oldRoot } = await createActiveTransaction(root); + await mkdir(join(newRoot, "review")); + await writeFile(join(newRoot, "review", "SKILL.md"), "staged contents", "utf8"); + await rename(dirname(mountPath), oldRoot); + + try { + await expect(materialize(createExecution(root, malformedSkill), logger)).rejects.toThrow( + "does not contain SKILL.md", + ); + await expect(readFile(join(mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "previous contents", + ); + await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("finishes a committed first catalog installation after a crash", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const mountPath = join(root, ".mosoo", "skill", "review"); + const malformedSkill = createSkill( + root, + createZipArchive({ "README.md": textEncoder.encode("missing skill markdown") }), + ); + const { activeRoot, newRoot } = await createActiveTransaction(root); + await mkdir(join(newRoot, "review")); + await writeFile(join(newRoot, "review", "SKILL.md"), "committed contents", "utf8"); + await writeFile(join(activeRoot, "COMMITTED"), ""); + + try { + await expect(materialize(createExecution(root, malformedSkill), logger)).rejects.toThrow( + "does not contain SKILL.md", + ); + await expect(readFile(join(mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "committed contents", + ); + await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("finishes a committed whole-catalog replacement after a crash", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const mountRoot = join(root, ".mosoo", "skill"); + const mountPath = join(mountRoot, "review"); + const malformedSkill = createSkill( + root, + createZipArchive({ "README.md": textEncoder.encode("missing skill markdown") }), + ); + await mkdir(mountPath, { recursive: true }); + await writeFile(join(mountPath, "SKILL.md"), "previous contents", "utf8"); + const { activeRoot, newRoot, oldRoot } = await createActiveTransaction(root); + await mkdir(join(newRoot, "review")); + await writeFile(join(newRoot, "review", "SKILL.md"), "committed contents", "utf8"); + await rename(mountRoot, oldRoot); + await writeFile(join(activeRoot, "COMMITTED"), ""); + + try { + await expect(materialize(createExecution(root, malformedSkill), logger)).rejects.toThrow( "does not contain SKILL.md", ); + await expect(readFile(join(mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "committed contents", + ); + await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("a rolled-back transaction does not conflict with the next catalog", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const mountPath = join(root, ".mosoo", "skill", "review"); + const currentSkill = createSkill( + root, + createZipArchive(createMarkdownSkillEntries("next catalog contents")), + ); + await mkdir(mountPath, { recursive: true }); + await writeFile(join(mountPath, "SKILL.md"), "previous contents", "utf8"); + const { newRoot, oldRoot } = await createActiveTransaction(root); + await mkdir(join(newRoot, "review")); + await writeFile(join(newRoot, "review", "SKILL.md"), "abandoned contents", "utf8"); + await rename(dirname(mountPath), oldRoot); + + try { + await materialize(createExecution(root, currentSkill), logger); + + await expect(readFile(join(mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "next catalog contents", + ); + await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("retires a committed transaction owner before a later catalog removes its mount", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const mountPath = join(root, ".mosoo", "skill", "review"); + await mkdir(mountPath, { recursive: true }); + await writeFile(join(mountPath, "SKILL.md"), "current contents", "utf8"); + const { oldRoot } = await createActiveTransaction(root, true); + await mkdir(join(oldRoot, "review")); + await Promise.all( + Array.from({ length: 1_025 }, (_, index) => + writeFile(join(oldRoot, "review", `old-${String(index)}`), "", "utf8"), + ), + ); + + try { + await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); + await expect(readFile(join(mountPath, "SKILL.md"), "utf8")).rejects.toThrow(); + + await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); + await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); + } finally { + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("a committed empty-catalog transaction does not conflict with re-adding the skill", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const skill = createSkill( + root, + createZipArchive(createMarkdownSkillEntries("re-added contents")), + ); + const { oldRoot } = await createActiveTransaction(root, true); + await mkdir(join(oldRoot, "review")); + await writeFile(join(oldRoot, "review", "SKILL.md"), "removed contents", "utf8"); + await mkdir(join(root, ".mosoo", "skill")); + + try { + await materialize(createExecution(root, skill), logger); + + await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "re-added contents", + ); + await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); } finally { await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); + test("a newer generation cancels an obsolete download before it can overwrite", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const oldArchive = createZipArchive(createMarkdownSkillEntries("obsolete contents")); + const newArchive = createZipArchive(createMarkdownSkillEntries("current contents")); + const oldRequestStarted = Promise.withResolvers(); + const nativeFetch = globalThis.fetch; + let oldRequestSignal: AbortSignal | null = null; + let oldStreamCancelled = false; + globalThis.fetch = (async ( + input: Parameters[0], + init?: Parameters[1], + ) => { + if (String(input) !== "https://skills.test/obsolete.zip") { + return nativeFetch(input, init); + } + + oldRequestSignal = init?.signal ?? null; + oldRequestStarted.resolve(); + return new Response( + new ReadableStream({ + cancel() { + oldStreamCancelled = true; + }, + start(controller) { + controller.enqueue(oldArchive); + }, + }), + ); + }) as unknown as typeof fetch; + const oldSkill = createSkill(root, oldArchive, { + downloadUrl: "https://skills.test/obsolete.zip", + }); + const newSkill = createSkill(root, newArchive, { + snapshotId: "snapshot-2" as DriverResolvedSkill["snapshotId"], + }); + try { + const oldMaterialization = materializeResolvedSkills( + createExecution(root, oldSkill), + logger, + new AbortController().signal, + ); + const oldResult = oldMaterialization.then( + () => null, + (error: unknown) => error, + ); + await oldRequestStarted.promise; + await materialize(createExecution(root, newSkill), logger); + + await expect(oldResult).resolves.toMatchObject({ + message: "Skill materialization was superseded by a newer generation.", + }); + expect((oldRequestSignal as AbortSignal | null)?.aborted).toBe(true); + expect(oldStreamCancelled).toBe(true); + await expect(readFile(join(newSkill.mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "current contents", + ); + } finally { + globalThis.fetch = nativeFetch; + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("cancels an oversized compressed stream before replacing the previous tree", async () => { + const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); + const logger = createTestLogger(); + const skill = createSkill(root, new Uint8Array(), { + downloadUrl: "https://skills.test/oversized.zip", + }); + const nativeFetch = globalThis.fetch; + let cancelled = false; + globalThis.fetch = (async () => { + return new Response( + new ReadableStream({ + cancel() { + cancelled = true; + }, + pull(controller) { + controller.enqueue(new Uint8Array(1024 * 1024)); + }, + }), + ); + }) as unknown as typeof fetch; + await mkdir(skill.mountPath, { recursive: true }); + await writeFile(join(skill.mountPath, "SKILL.md"), "previous contents", "utf8"); + + try { + await expect(materialize(createExecution(root, skill), logger)).rejects.toThrow( + "Compressed skill package exceeds the limit", + ); + expect(cancelled).toBe(true); + await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).resolves.toBe( + "previous contents", + ); + } finally { + globalThis.fetch = nativeFetch; + await logger.destroy(); + await rm(root, { force: true, recursive: true }); + } + }); + test("exposes a relative native alias and removes it when the skill becomes unavailable", async () => { const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); const logger = createTestLogger(); @@ -168,16 +1026,16 @@ Check the diff.`), const aliasPath = join(root, ".agents", "skills", "review"); try { - const materialized = await materializeResolvedSkills(execution, logger); + const materializedSkills = await materialize(execution, logger); - await exposeNativeSkillAliases(execution, logger, materialized); + await exposeAliases(execution, logger, materializedSkills); await expect(readlink(aliasPath)).resolves.toBe("../../.mosoo/skill/review"); await expect(readFile(join(aliasPath, "SKILL.md"), "utf8")).resolves.toContain( "Check the diff.", ); - await exposeNativeSkillAliases(execution, logger, []); + await exposeAliases(execution, logger, []); await expect(readFile(join(aliasPath, "SKILL.md"), "utf8")).rejects.toThrow(); } finally { @@ -202,15 +1060,15 @@ Check the diff.`), } satisfies AgentDriverMaterializedSkill; try { - await expect(exposeNativeSkillAliases(execution, logger, [skill])).resolves.toEqual([]); - await expect(readdir(join(root, ".agents", "skills"))).resolves.toEqual([]); + await expect(exposeAliases(execution, logger, [skill])).resolves.toEqual([]); + await expect(readdir(join(root, ".agents", "skills"))).rejects.toThrow(); } finally { await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); - test("keeps the first skill when native skill names collide", async () => { + test("rejects duplicate native skill names instead of silently choosing one", async () => { const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); const logger = createTestLogger(); const execution = createExecution( @@ -235,12 +1093,10 @@ Check the diff.`), } satisfies AgentDriverMaterializedSkill; try { - await expect( - exposeNativeSkillAliases(execution, logger, [first, duplicate]), - ).resolves.toEqual([join(root, ".agents", "skills", "review")]); - await expect(readlink(join(root, ".agents", "skills", "review"))).resolves.toBe( - "../../.mosoo/skill/skill-1", + await expect(exposeAliases(execution, logger, [first, duplicate])).rejects.toThrow( + "duplicate skill name", ); + await expect(readlink(join(root, ".agents", "skills", "review"))).rejects.toThrow(); } finally { await logger.destroy(); await rm(root, { force: true, recursive: true }); @@ -263,7 +1119,7 @@ Check the diff.`), await symlink("../../.mosoo/skill/gone", aliasPath, "dir"); try { - await exposeNativeSkillAliases(execution, logger, [ + await exposeAliases(execution, logger, [ { mountPath, skillId: "skill-2", @@ -274,7 +1130,7 @@ Check the diff.`), ]); await expect(readlink(aliasPath)).resolves.toBe("../../.mosoo/skill/skill-2"); - await exposeNativeSkillAliases(execution, logger, []); + await exposeAliases(execution, logger, []); await expect(readlink(aliasPath)).rejects.toThrow(); } finally { @@ -299,7 +1155,7 @@ Check the diff.`), try { await expect( - exposeNativeSkillAliases(execution, logger, [ + exposeAliases(execution, logger, [ { mountPath, skillId: "skill-1", From 994f2e092a85640d16afcfdb914076c8b251fca2 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Thu, 27 Aug 2026 17:39:28 +0000 Subject: [PATCH 03/20] refactor: simplify runtime and test infrastructure --- README.md | 4 +- package.json | 2 +- src/bin/driver-process.ts | 6 +- src/core/agent-backend-lifecycle.ts | 26 +- src/core/agent-driver-kernel.ts | 16 +- src/core/driver-command-dispatcher.ts | 34 +- src/index.ts | 3 +- src/observability/index.ts | 4 + .../acp/acp-assistant-transcript-state.ts | 44 +-- src/runtimes/acp/acp-driver-backend.ts | 5 - src/runtimes/atomic-file.ts | 134 +++++--- src/runtimes/claude/agent-sdk-event-writer.ts | 98 ++---- .../claude/agent-sdk-message-translator.ts | 145 ++++----- src/runtimes/claude/agent-sdk-tool-id.ts | 36 +- src/runtimes/openai/app-server-client.ts | 14 +- src/runtimes/openai/app-server-event-state.ts | 40 +-- .../app-server-protocol-client-schemas.ts | 60 ++-- .../openai/app-server-protocol-client.ts | 21 -- .../openai/app-server-protocol-server.ts | 31 +- src/runtimes/openai/app-server-protocol.ts | 2 +- src/runtimes/openai/auth-state.ts | 51 +-- src/runtimes/provider-registry.ts | 40 +-- src/runtimes/runtime-public-id-state.ts | 29 ++ src/runtimes/skill-bootstrap.ts | 187 ++++------- src/runtimes/skill-materialization.ts | 154 ++------- src/surfaces/cma-http/request.ts | 63 +--- src/surfaces/cma-sdk/client.ts | 200 +++--------- src/surfaces/cma-sdk/index.ts | 3 +- src/surfaces/cma-sdk/types.ts | 43 +-- src/utils/async.ts | 61 ++++ tests/acp-agent-process.test.ts | 19 +- tests/acp-configuration.test.ts | 10 +- tests/acp-driver-backend.test.ts | 16 +- tests/acp-event-translator-lifecycle.test.ts | 24 +- tests/acp-file-system.test.ts | 68 ++-- tests/acp-terminal-manager.test.ts | 54 +-- tests/agent-backend-lifecycle.test.ts | 52 +-- tests/architecture-boundaries.test.ts | 41 +-- tests/claude-agent-sdk-driver-backend.test.ts | 41 +-- tests/claude-agent-sdk-durability.test.ts | 46 +-- ...ent-sdk-provider-fixtures-terminal.test.ts | 223 ++----------- ...t-sdk-provider-fixtures-transcript.test.ts | 129 ++------ tests/claude-agent-sdk-query-options.test.ts | 18 +- tests/claude-agent-sdk-test-helpers.ts | 46 +++ tests/cma-sdk.test.ts | 27 +- tests/driver-artifact-contract.test.ts | 42 ++- .../driver-event-publisher-admission.test.ts | 147 ++------- ...river-event-publisher-backpressure.test.ts | 61 +--- tests/driver-event-publisher-drain.test.ts | 69 +--- tests/driver-event-publisher-fixture.ts | 51 +++ tests/driver-heartbeat-loop.test.ts | 18 +- tests/driver-instance-socket-process.test.ts | 125 ++----- ...driver-runtime-boundary-dispatcher.test.ts | 14 - ...er-runtime-boundary-event-envelope.test.ts | 5 - tests/driver-runtime-boundary-fixtures.ts | 8 +- tests/driver-runtime-boundary-state.test.ts | 31 +- tests/driver-runtime-boundary-timing.test.ts | 10 - tests/openai-app-server-auth-state.test.ts | 15 +- tests/openai-app-server-client.test.ts | 45 +-- .../openai-app-server-event-bridge-fixture.ts | 10 +- ...enai-app-server-event-bridge-items.test.ts | 40 +-- ...-server-event-bridge-run-lifecycle.test.ts | 36 +- ...i-app-server-event-bridge-terminal.test.ts | 105 ++---- ...app-server-event-bridge-transcript.test.ts | 27 +- ...penai-app-server-provider-fixtures.test.ts | 308 +++--------------- .../openai-app-server-request-handler.test.ts | 26 +- tests/openai-app-server-startup.test.ts | 42 +-- tests/opencode-acp-contract.test.ts | 9 +- tests/provider-fixture-test-helpers.ts | 159 +++++++++ tests/public-api-exports.snapshot.json | 4 +- tests/public-api.test.ts | 8 +- tests/skill-materialization.test.ts | 45 +-- tsconfig.types.json | 12 - vite.config.ts | 11 + 74 files changed, 1226 insertions(+), 2627 deletions(-) delete mode 100644 src/runtimes/openai/app-server-protocol-client.ts create mode 100644 src/runtimes/runtime-public-id-state.ts create mode 100644 tests/driver-event-publisher-fixture.ts create mode 100644 tests/provider-fixture-test-helpers.ts delete mode 100644 tsconfig.types.json create mode 100644 vite.config.ts diff --git a/README.md b/README.md index e765cb9..b405591 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ import { expect, test } from "bun:test"; import { createCmaMemoryStore } from "@mosoo/agent-driver"; import { createCmaHttpHandler } from "@mosoo/agent-driver/cma-http"; -import { createCmaSdkClient } from "@mosoo/agent-driver/cma-sdk"; +import { CmaSdkClient } from "@mosoo/agent-driver/cma-sdk"; test("create an agent, environment, and session over the CMA surface", async () => { // 1. An in-memory store stands in for the host's persistence port. @@ -119,7 +119,7 @@ test("create an agent, environment, and session over the CMA surface", async () // 3. The client talks to the handler directly through fetch — point // baseUrl at a server that explicitly embeds this preview. The default beta header // (anthropic-beta: managed-agents-2026-04-01) is sent automatically. - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.local", fetch: async (input, init) => handler(new Request(input, init)), }); diff --git a/package.json b/package.json index b8c9c8a..f0270ea 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "test:live:opencode": "vp run build && AGENT_DRIVER_LIVE=1 AGENT_DRIVER_LIVE_SUITE=opencode bun test tests/driver-artifact-live.test.ts", "bench": "bun bench/ttft-bench.ts", "build": "vp run build:types && bun build src/bin/driver.ts --target bun --outfile dist/driver.mjs", - "build:types": "rm -rf dist/types && vp exec tsc -p tsconfig.types.json", + "build:types": "vp pack", "build:image": "vp run build && buildah build --http-proxy=false --platform linux/amd64 -t agent-driver:local .", "test:image:environment": "podman run --pull=never --rm --entrypoint node agent-driver:local /usr/local/libexec/mosoo/environment-package-manager-check.mjs smoke", "check": "vp check && vp run tc && vp run test && vp run build", diff --git a/src/bin/driver-process.ts b/src/bin/driver-process.ts index 89dc156..ca6d799 100644 --- a/src/bin/driver-process.ts +++ b/src/bin/driver-process.ts @@ -40,10 +40,6 @@ import { promiseWithTimeout } from "../utils/async"; const DRIVER_BACKEND_START_TIMEOUT_MS = 60_000; const DRIVER_SHUTDOWN_TIMEOUT_MS = 5_000; -function parseNullableRunId(value: string | null): RunId | null { - return value === null ? null : parseRunId(value); -} - export class DriverProcess { readonly #startedAt = new Date().toISOString(); readonly #backendFactory: AgentDriverBackendFactory; @@ -135,7 +131,7 @@ export class DriverProcess { startedAt: this.#startedAt, }), ); - const initialRunId = parseNullableRunId(hello.runId); + const initialRunId = hello.runId === null ? null : parseRunId(hello.runId); // The server accepts pushLogs only after hello commits; release the // buffered boot logs now instead of racing the handshake round-trip. uplink.open(); diff --git a/src/core/agent-backend-lifecycle.ts b/src/core/agent-backend-lifecycle.ts index 6cc1dc2..7030b40 100644 --- a/src/core/agent-backend-lifecycle.ts +++ b/src/core/agent-backend-lifecycle.ts @@ -37,10 +37,6 @@ type DeferredCleanup = | { after: Promise; kind: "final_stop" } | { generation: number; kind: "late_stop"; task: Promise }; -type StopOperation = { - settled: Promise; -}; - export class AgentBackendLifecycle { readonly #backend: AgentDriverBackend; readonly #createContext: () => AgentDriverContext; @@ -53,7 +49,7 @@ export class AgentBackendLifecycle { readonly #startTimeoutMs: number; readonly #stopTimeoutMs: number; #cleanupOwner: CleanupOwner | null = null; - #inFlightStop: StopOperation | null = null; + #inFlightStop: Promise | null = null; #startController: AbortController | null = null; #startupStopTask: Promise | null = null; #startTask: Promise | null = null; @@ -170,7 +166,7 @@ export class AgentBackendLifecycle { timeoutMs: this.#remaining(owner), }); if (startResult.status === "timed_out") { - const stopSettled = this.#inFlightStop?.settled ?? Promise.resolve(); + const stopSettled = this.#inFlightStop ?? Promise.resolve(); owner.deferred = { after: Promise.allSettled([startTask, stopSettled]).then(() => {}), kind: "final_stop", @@ -188,13 +184,13 @@ export class AgentBackendLifecycle { async #stop(owner: CleanupOwner, reason: string, label: string, final: boolean): Promise { const previous = this.#inFlightStop; if (previous !== null) { - const previousResult = await settlePromiseWithTimeout(previous.settled, { + const previousResult = await settlePromiseWithTimeout(previous, { label, timeoutMs: this.#remaining(owner), }); if (previousResult.status !== "completed") { if (final && previousResult.status === "timed_out") { - owner.deferred = { after: previous.settled, kind: "final_stop" }; + owner.deferred = { after: previous, kind: "final_stop" }; } throw previousResult.error; } @@ -205,14 +201,12 @@ export class AgentBackendLifecycle { const task = Promise.resolve().then(() => this.#runStop(this.#backend, this.#createContext(), reason, controller.signal), ); - const operation: StopOperation = { - settled: Promise.resolve(), - }; - operation.settled = task.then( - () => this.#clearStop(operation), - () => this.#clearStop(operation), + let settled: Promise; + settled = task.then( + (): void => this.#clearStop(settled), + (): void => this.#clearStop(settled), ); - this.#inFlightStop = operation; + this.#inFlightStop = settled; const result = await settlePromiseWithTimeout(task, { label, @@ -230,7 +224,7 @@ export class AgentBackendLifecycle { throw result.error; } - #clearStop(operation: StopOperation): void { + #clearStop(operation: Promise): void { if (this.#inFlightStop === operation) { this.#inFlightStop = null; } diff --git a/src/core/agent-driver-kernel.ts b/src/core/agent-driver-kernel.ts index b8dd6b1..66ec98a 100644 --- a/src/core/agent-driver-kernel.ts +++ b/src/core/agent-driver-kernel.ts @@ -1,4 +1,4 @@ -import { createBufferedSinkLogger } from "../observability"; +import { createDisabledLogger } from "../observability"; import type { Logger } from "../observability"; import type { DriverEventInput } from "../protocol/events"; import { createDriverId, type RunId } from "../protocol/id"; @@ -60,14 +60,6 @@ function jsonBytes(value: unknown): number { return Buffer.byteLength(JSON.stringify(value), "utf8"); } -function createKernelLogger(): Logger { - return createBufferedSinkLogger({ - level: "debug", - service: "agent-driver-kernel", - sink: async () => {}, - }); -} - function toDispatchError(error: RunError | undefined, command: RuntimeCommand): Error { if (!error) { return new Error(`Driver command ${command.kind} failed.`); @@ -122,7 +114,7 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo this.#backendFactory = options.backendFactory; this.#externalToolEffectLedger = options.externalToolEffectLedger; this.#hostPorts = options.hostPorts; - this.#logger = options.logger ?? createKernelLogger(); + this.#logger = options.logger ?? createDisabledLogger(); this.#permissionBroker = new DriverPermissionBroker(() => this.#logger); } @@ -344,10 +336,6 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo return this.#runEventTerminal?.runId === runId ? this.#runEventTerminal.status : null; } - selectedRunEventTerminal(runId: RunId): "cancelled" | "completed" | "failed" | null { - return this.runEventTerminal(runId); - } - async start(input: AgentDriverKernelStartInput): Promise { if (this.#runtimeState.status() !== "created") { throw new Error("Driver kernel has already started."); diff --git a/src/core/driver-command-dispatcher.ts b/src/core/driver-command-dispatcher.ts index dbabe79..d43786d 100644 --- a/src/core/driver-command-dispatcher.ts +++ b/src/core/driver-command-dispatcher.ts @@ -40,7 +40,6 @@ interface DriverCommandDispatcherOptions { const COMMAND_POLL_INTERVAL_MS = 250; export const ACTIVE_TURN_CANCEL_GRACE_MS = 2_000; const ACTIVE_INPUT_SETTLE_GRACE_MS = ACTIVE_TURN_CANCEL_GRACE_MS + DRIVER_EVENT_DELIVERY_TIMEOUT_MS; -const EXTERNAL_TOOL_EFFECT_COMPLETE_ATTEMPTS = 2; const EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS = 2_000; const MAX_ACTIVE_MCP_COMMANDS = 32; @@ -579,6 +578,7 @@ export class DriverCommandDispatcher { ) { throw new Error("Driver external tool effect ledger is not configured."); } + const completeExternalToolEffect = effectLedger.completeExternalToolEffect.bind(effectLedger); try { await pushLosslessEvents(socket, [ @@ -621,27 +621,17 @@ export class DriverCommandDispatcher { const execution = await prepared.execute(claim); const { providerReceiptJson, ...executionResult } = execution; result = executionResult; - let completionFailure: { error: unknown } | null = null; - for (let attempt = 0; attempt < EXTERNAL_TOOL_EFFECT_COMPLETE_ATTEMPTS; attempt += 1) { - try { - await effectLedger.completeExternalToolEffect( - { - commandId: command.commandId, - ...(providerReceiptJson === undefined ? {} : { providerReceiptJson }), - result, - }, - AbortSignal.timeout(EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS), - ); - durableResult = result; - completionFailure = null; - break; - } catch (error) { - completionFailure = { error }; - } - } - if (completionFailure !== null) { - throw completionFailure.error; - } + const complete = () => + completeExternalToolEffect( + { + commandId: command.commandId, + ...(providerReceiptJson === undefined ? {} : { providerReceiptJson }), + result, + }, + AbortSignal.timeout(EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS), + ); + await Promise.resolve().then(complete).catch(complete); + durableResult = result; } await pushLosslessEvents(socket, [ diff --git a/src/index.ts b/src/index.ts index e9bac88..9e7956e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,10 +41,9 @@ export type { CmaHttpHandler, CmaHttpHandlerOptions, } from "./surfaces/cma-http"; -export { CmaSdkError, createCmaSdkClient } from "./surfaces/cma-sdk"; +export { CmaSdkClient, CmaSdkError } from "./surfaces/cma-sdk"; export type { CmaSdkBetaHeader, - CmaSdkClient, CmaSdkClientOptions, CmaSdkFetch, CmaSdkRequestOptions, diff --git a/src/observability/index.ts b/src/observability/index.ts index a0c5dff..7e1ba08 100644 --- a/src/observability/index.ts +++ b/src/observability/index.ts @@ -348,6 +348,10 @@ function createBaseLogger(options: BaseLoggerOptions): Logger { return createLogger(config); } +export function createDisabledLogger(): Logger { + return createLogger({ enabled: false, transports: [] }); +} + export function createBufferedSinkLogger(options: CreateBufferedSinkLoggerOptions): Logger { const logger = createBaseLogger(options); const level = options.level ?? "info"; diff --git a/src/runtimes/acp/acp-assistant-transcript-state.ts b/src/runtimes/acp/acp-assistant-transcript-state.ts index ca3776b..ccad62b 100644 --- a/src/runtimes/acp/acp-assistant-transcript-state.ts +++ b/src/runtimes/acp/acp-assistant-transcript-state.ts @@ -86,19 +86,10 @@ export class AcpAssistantTranscriptState { } begin(input: AcpAssistantTranscriptStateInput): void { - this.#activeAssistantMessage = null; - this.#lastCompletedAssistantMessage = null; + this.clear(); this.#promptMessageId = input.messageId; this.#runId = input.runId; - this.#sequence = 0; - this.#settledAssistantNativeMessageIds.clear(); - this.#settledAssistantNativeMessageIdBytes = 0; - this.#thoughtCompleted = false; - this.#thoughtFallbackText = ""; - this.#thoughtFallbackNativeMessageId = null; this.#thoughtId = `${input.messageId}:thought`; - this.#thoughtStarted = false; - this.#tools.clear(); } clear(): void { @@ -778,43 +769,24 @@ export class AcpAssistantTranscriptState { return null; } - const active = this.#activeAssistantMessage; - - if (active?.nativeMessageId === nativeMessageId) { - return { events: [], message: active }; - } - - const events = this.#finishMessage(); - const message: AcpAssistantMessageState = { - id: createDriverId(), - nativeMessageId, - text: "", - }; - - this.#activeAssistantMessage = message; - this.#clearThoughtFallback(); - events.push({ - kind: "message.started", - payload: { - messageId: message.id, - role: "agent", - }, - runId: this.#requireRunId(), - }); - return { events, message }; + return this.#startUnsettledMessage(nativeMessageId); } #startAnonymousMessage(): AcpAssistantMessageStart { + return this.#startUnsettledMessage(null); + } + + #startUnsettledMessage(nativeMessageId: string | null): AcpAssistantMessageStart { const active = this.#activeAssistantMessage; - if (active?.nativeMessageId === null) { + if (active?.nativeMessageId === nativeMessageId) { return { events: [], message: active }; } const events = this.#finishMessage(); const message: AcpAssistantMessageState = { id: createDriverId(), - nativeMessageId: null, + nativeMessageId, text: "", }; diff --git a/src/runtimes/acp/acp-driver-backend.ts b/src/runtimes/acp/acp-driver-backend.ts index 65c394b..b41ef05 100644 --- a/src/runtimes/acp/acp-driver-backend.ts +++ b/src/runtimes/acp/acp-driver-backend.ts @@ -157,11 +157,6 @@ export class AcpDriverBackend implements AgentDriverBackend { throw new Error("ACP driver backend stopped during startup."); } - signal.throwIfAborted(); - if (this.#stopRequested) { - throw new Error("ACP driver backend stopped during startup."); - } - const setup = await this.#connect(context, signal, true); if (setup.mode === "created" && this.#nativeInstructionPath === null) { diff --git a/src/runtimes/atomic-file.ts b/src/runtimes/atomic-file.ts index 771d14b..3818ea4 100644 --- a/src/runtimes/atomic-file.ts +++ b/src/runtimes/atomic-file.ts @@ -2,9 +2,9 @@ import { randomUUID } from "node:crypto"; import { constants, type Dirent } from "node:fs"; import { lstat, mkdir, open, opendir, rename, unlink } from "node:fs/promises"; import type { FileHandle } from "node:fs/promises"; -import { basename, join, resolve } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; -function hasErrorCode(error: unknown, code: string): boolean { +export function hasErrorCode(error: unknown, code: string): boolean { return typeof error === "object" && error !== null && "code" in error && error.code === code; } @@ -12,6 +12,7 @@ const activeAtomicWriteTemporaryFiles = new Set(); const ATOMIC_WRITE_TEMPORARY_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; const MAX_ATOMIC_WRITE_TEMPORARY_SCAN_ENTRIES = 256; +const NON_ABORTING_SIGNAL = AbortSignal.any([]); export async function closeFileHandles( handles: readonly (FileHandle | null | undefined)[], @@ -55,24 +56,40 @@ export async function openRealDirectory(path: string, label: string): Promise { try { - if (!(await directory.stat()).isDirectory()) { - throw new Error(`${label} must be a real directory: ${path}.`); + return await openRealDirectory(path, label); + } catch (error) { + if (hasErrorCode(error, "ENOENT")) { + return null; } - return directory; + throw error; + } +} + +export async function readPathStats( + path: string, +): Promise> | null> { + try { + return await lstat(path); } catch (error) { - return closeFileHandlesAndThrow(error, [directory], `Failed to open ${path}.`); + if (hasErrorCode(error, "ENOENT")) { + return null; + } + throw error; } } @@ -111,17 +128,17 @@ export async function ensureRealDirectoryAt( } } -async function walkAbsoluteRealDirectory( - path: string, +async function walkRealDirectory( + startPath: string, + segments: readonly string[], label: string, create: boolean, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); - const absolutePath = resolve(path); - let directory = await openRealDirectory("/", label); + let directory = await openRealDirectory(startPath, label); - for (const segment of absolutePath.split("/").filter(Boolean)) { + for (const segment of segments) { let next: FileHandle; try { signal?.throwIfAborted(); @@ -129,15 +146,15 @@ async function walkAbsoluteRealDirectory( ? await ensureRealDirectoryAt(directory, segment, label, signal) : await openRealDirectory(directoryEntryPath(directory, segment), label); } catch (error) { - return closeFileHandlesAndThrow(error, [directory], `Failed to walk ${absolutePath}.`); + return closeFileHandlesAndThrow(error, [directory], `Failed to walk ${startPath}.`); } const closeFailures = await closeFileHandles([directory]); if (closeFailures.length > 0) { return closeFileHandlesAndThrow( - new AggregateError(closeFailures, `Failed to close an ancestor of ${absolutePath}.`), + new AggregateError(closeFailures, `Failed to close an ancestor of ${startPath}.`), [next], - `Failed to walk ${absolutePath}.`, + `Failed to walk ${startPath}.`, ); } directory = next; @@ -147,7 +164,8 @@ async function walkAbsoluteRealDirectory( } export function openAbsoluteRealDirectory(path: string, label: string): Promise { - return walkAbsoluteRealDirectory(path, label, false); + const absolutePath = resolve(path); + return walkRealDirectory("/", absolutePath.split("/").filter(Boolean), label, false); } export function ensureAbsoluteRealDirectory( @@ -155,7 +173,24 @@ export function ensureAbsoluteRealDirectory( label: string, signal?: AbortSignal, ): Promise { - return walkAbsoluteRealDirectory(path, label, true, signal); + const absolutePath = resolve(path); + return walkRealDirectory("/", absolutePath.split("/").filter(Boolean), label, true, signal); +} + +export function openRelativeRealDirectory( + root: FileHandle, + path: string, + label: string, + create: boolean, + signal?: AbortSignal, +): Promise { + return walkRealDirectory( + `${openedDirectoryPath(root)}/.`, + path === "." ? [] : path.split("/"), + label, + create, + signal, + ); } export async function assertDirectoryIdentity( @@ -164,16 +199,7 @@ export async function assertDirectoryIdentity( label: string, ): Promise { const openedStats = await directory.stat(); - let pathStats: Awaited> | null; - - try { - pathStats = await lstat(path); - } catch (error) { - if (!hasErrorCode(error, "ENOENT")) { - throw error; - } - pathStats = null; - } + const pathStats = await readPathStats(path); if ( pathStats === null || @@ -186,13 +212,6 @@ export async function assertDirectoryIdentity( } } -export async function isActiveAtomicWriteTemporaryFile( - directory: FileHandle, - name: string, -): Promise { - return activeAtomicWriteTemporaryFiles.has(await atomicWriteTemporaryFileKey(directory, name)); -} - export async function cleanupAtomicWriteTemporaryFiles( directory: FileHandle, targetNames: readonly string[], @@ -216,7 +235,10 @@ export async function cleanupAtomicWriteTemporaryFiles( entry.name.endsWith(".tmp") && ATOMIC_WRITE_TEMPORARY_ID_PATTERN.test(entry.name.slice(name.length + 2, -".tmp".length)), ); - if (target === undefined || (await isActiveAtomicWriteTemporaryFile(directory, entry.name))) { + if ( + target === undefined || + activeAtomicWriteTemporaryFiles.has(await atomicWriteTemporaryFileKey(directory, entry.name)) + ) { continue; } if (entry.isDirectory() && !entry.isSymbolicLink()) { @@ -291,3 +313,41 @@ export async function writeFileAtomically( activeAtomicWriteTemporaryFiles.delete(temporaryKey); } } + +export async function writeFileAtomicallyAtPath( + path: string, + contents: string, + options: { mode: number; skipIfUnchanged?: boolean }, +): Promise { + const absolutePath = resolve(path); + const directoryPath = dirname(absolutePath); + const name = basename(absolutePath); + await using directory = await ensureAbsoluteRealDirectory( + directoryPath, + "Atomic file parent", + NON_ABORTING_SIGNAL, + ); + await cleanupAtomicWriteTemporaryFiles(directory, [name], NON_ABORTING_SIGNAL); + + let written = true; + if (options.skipIfUnchanged === true) { + const existing = await (async (): Promise => { + try { + await using file = await open( + directoryEntryPath(directory, name), + constants.O_RDONLY | constants.O_NOFOLLOW, + ); + return (await file.stat()).isFile() ? await file.readFile("utf8") : null; + } catch { + return null; + } + })(); + written = existing !== contents; + } + + if (written) { + await writeFileAtomically(directory, name, contents, options.mode, NON_ABORTING_SIGNAL); + } + await assertDirectoryIdentity(directory, directoryPath, "Atomic file parent"); + return written; +} diff --git a/src/runtimes/claude/agent-sdk-event-writer.ts b/src/runtimes/claude/agent-sdk-event-writer.ts index dfdabd4..80c4eac 100644 --- a/src/runtimes/claude/agent-sdk-event-writer.ts +++ b/src/runtimes/claude/agent-sdk-event-writer.ts @@ -1,5 +1,6 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; +import type { ProtocolError } from "../../contract"; import type { DriverEventInput } from "../../protocol/events"; import type { MessageId, RunId } from "../../protocol/id"; import type { JsonValue } from "../../protocol/json"; @@ -90,6 +91,10 @@ export interface ClaudeTurnClosure { readonly events: readonly DriverEventInput[]; } +type ClaudeMessageSettlement = + | { readonly status: "cancelled" | "completed" } + | { readonly error: ProtocolError; readonly status: "failed" }; + export class ClaudeAgentSdkEventWriter { readonly #messageEnded = new Set(); readonly #messageRetracted = new Set(); @@ -128,35 +133,32 @@ export class ClaudeAgentSdkEventWriter { return this.#toolParentMessage.get(toolCallId) ?? null; } - async endMessage(context: AgentDriverContext, messageId: string): Promise { - if (!this.#messageStarted.has(messageId) || this.#messageEnded.has(messageId)) { - return false; - } - - await this.#options.push(context, "driver.claude.message.ended", [ - { - kind: "message.completed", - payload: { - messageId, - role: "agent", - }, - }, - ]); - this.#messageEnded.add(messageId); - return true; - } - - async cancelMessage(context: AgentDriverContext, messageId: string): Promise { + async settleMessage( + context: AgentDriverContext, + messageId: string, + settlement: ClaudeMessageSettlement, + ): Promise { if (!this.#messageStarted.has(messageId) || this.#messageEnded.has(messageId)) { return false; } - await this.#options.push(context, "driver.claude.message.cancelled", [ - { - kind: "message.cancelled", - payload: { messageId, role: "agent" }, - }, - ]); + const event: DriverEventInput = + settlement.status === "failed" + ? { + kind: "message.failed", + payload: { error: settlement.error, messageId, role: "agent" }, + } + : { + kind: settlement.status === "completed" ? "message.completed" : "message.cancelled", + payload: { messageId, role: "agent" }, + }; + await this.#options.push( + context, + settlement.status === "completed" + ? "driver.claude.message.ended" + : `driver.claude.message.${settlement.status}`, + [event], + ); this.#messageEnded.add(messageId); return true; } @@ -200,25 +202,6 @@ export class ClaudeAgentSdkEventWriter { this.#toolEnded.add(toolCallId); } - async failMessage( - context: AgentDriverContext, - messageId: string, - error: { readonly code: string; readonly message: string; readonly retryable: boolean }, - ): Promise { - if (!this.#messageStarted.has(messageId) || this.#messageEnded.has(messageId)) { - return false; - } - - await this.#options.push(context, "driver.claude.message.failed", [ - { - kind: "message.failed", - payload: { error, messageId, role: "agent" }, - }, - ]); - this.#messageEnded.add(messageId); - return true; - } - async ensureMessageStarted(context: AgentDriverContext, messageId: string): Promise { if (this.#messageStarted.has(messageId)) { return; @@ -517,31 +500,18 @@ export class ClaudeAgentSdkEventWriter { ]); } - async endThought(context: AgentDriverContext, thoughtId: string): Promise { - if (!this.#thoughtStarted.has(thoughtId) || this.#thoughtEnded.has(thoughtId)) { - return; - } - - await this.#options.push(context, "driver.claude.thought.completed", [ - { - kind: "thought.completed", - payload: { - channel: "summary", - thoughtId, - }, - }, - ]); - this.#thoughtEnded.add(thoughtId); - } - - async cancelThought(context: AgentDriverContext, thoughtId: string): Promise { + async settleThought( + context: AgentDriverContext, + thoughtId: string, + status: "cancelled" | "completed", + ): Promise { if (!this.#thoughtStarted.has(thoughtId) || this.#thoughtEnded.has(thoughtId)) { return; } - await this.#options.push(context, "driver.claude.thought.cancelled", [ + await this.#options.push(context, `driver.claude.thought.${status}`, [ { - kind: "thought.cancelled", + kind: status === "completed" ? "thought.completed" : "thought.cancelled", payload: { channel: "summary", thoughtId }, }, ]); diff --git a/src/runtimes/claude/agent-sdk-message-translator.ts b/src/runtimes/claude/agent-sdk-message-translator.ts index 3b7bf74..8aa90b1 100644 --- a/src/runtimes/claude/agent-sdk-message-translator.ts +++ b/src/runtimes/claude/agent-sdk-message-translator.ts @@ -84,12 +84,12 @@ export class ClaudeAgentSdkMessageTranslator { async #cancelOpenTurn(context: AgentDriverContext): Promise { for (const [messageId, thoughtId] of this.#state.activeThoughts()) { - await this.#events.cancelThought(context, thoughtId); + await this.#events.settleThought(context, thoughtId, "cancelled"); this.#state.deleteThoughtId(messageId); } for (const [messageId, runId] of this.#state.assistantMessages()) { - await this.#events.cancelMessage(context, messageId); + await this.#events.settleMessage(context, messageId, { status: "cancelled" }); this.#state.completeAssistantMessage(runId, messageId, false); } @@ -153,7 +153,7 @@ export class ClaudeAgentSdkMessageTranslator { const thoughtId = this.#state.thoughtIdForMessage(messageId); if (thoughtId !== undefined) { - await this.#events.endThought(context, thoughtId); + await this.#events.settleThought(context, thoughtId, "completed"); this.#state.deleteThoughtId(messageId); } } @@ -299,11 +299,11 @@ export class ClaudeAgentSdkMessageTranslator { if (outcome.status === "cancelled") { const thoughtId = this.#state.thoughtIdForMessage(messageId); if (thoughtId !== undefined) { - await this.#events.cancelThought(context, thoughtId); + await this.#events.settleThought(context, thoughtId, "cancelled"); this.#state.deleteThoughtId(messageId); } await this.#events.ensureMessageStarted(context, messageId); - await this.#events.cancelMessage(context, messageId); + await this.#events.settleMessage(context, messageId, { status: "cancelled" }); this.#state.completeAssistantMessage(runId, messageId, false); return; } @@ -311,10 +311,13 @@ export class ClaudeAgentSdkMessageTranslator { if (outcome.status === "failed") { await this.#endThought(context, messageId); await this.#events.ensureMessageStarted(context, messageId); - await this.#events.failMessage(context, messageId, { - code: `claude.${outcome.code}`, - message: outcome.message, - retryable: outcome.retryable, + await this.#events.settleMessage(context, messageId, { + error: { + code: `claude.${outcome.code}`, + message: outcome.message, + retryable: outcome.retryable, + }, + status: "failed", }); this.#state.completeAssistantMessage(runId, messageId, false); return; @@ -576,7 +579,7 @@ export class ClaudeAgentSdkMessageTranslator { runId: RunId, messageId: MessageId, ): Promise { - const ended = await this.#events.endMessage(context, messageId); + const ended = await this.#events.settleMessage(context, messageId, { status: "completed" }); this.#state.completeAssistantMessage(runId, messageId, ended); } @@ -709,7 +712,7 @@ export class ClaudeAgentSdkMessageTranslator { if (messageId !== null) { const thoughtId = this.#state.thoughtIdForMessage(messageId); if (thoughtId !== undefined) { - await this.#events.cancelThought(context, thoughtId); + await this.#events.settleThought(context, thoughtId, "cancelled"); this.#state.deleteThoughtId(messageId); } await this.#events.retractMessage(context, messageId); @@ -742,7 +745,7 @@ export class ClaudeAgentSdkMessageTranslator { const messageId = this.#state.auxiliaryMessageId(runId, nativeMessageId); if (await this.#events.pushMessageSnapshot(context, messageId, content, metadata)) { - await this.#events.endMessage(context, messageId); + await this.#events.settleMessage(context, messageId, { status: "completed" }); } } @@ -822,18 +825,24 @@ export class ClaudeAgentSdkMessageTranslator { aggregateClaudeModelUsage(message.modelUsage), message.total_cost_usd, ); - - if (cancelled) { + const finishResult = async ( + toolStatus: "cancelled" | "completed" | "failed", + terminal: DriverEventInput, + reason: string, + ): Promise => { try { - await this.finishTurnWithTerminal( - context, - "cancelled", - this.#events.runCancelled(runId, message.terminal_reason ?? "provider.aborted"), - "driver.claude.turn.cancelled", - ); + await this.finishTurnWithTerminal(context, toolStatus, terminal, reason); } catch (error) { throw new ClaudeTerminalWriteError(error); } + }; + + if (cancelled) { + await finishResult( + "cancelled", + this.#events.runCancelled(runId, message.terminal_reason ?? "provider.aborted"), + "driver.claude.turn.cancelled", + ); return; } @@ -845,21 +854,16 @@ export class ClaudeAgentSdkMessageTranslator { : jsonValueSchema.safeParse(message.structured_output); if (structuredOutput !== undefined && !structuredOutput.success) { - try { - await this.finishTurnWithTerminal( - context, - "failed", - this.#events.runError( - runId, - "claude.invalid_structured_output", - "Claude Agent SDK returned a non-JSON structured output.", - false, - ), - "driver.claude.turn.failed", - ); - } catch (error) { - throw new ClaudeTerminalWriteError(error); - } + await finishResult( + "failed", + this.#events.runError( + runId, + "claude.invalid_structured_output", + "Claude Agent SDK returned a non-JSON structured output.", + false, + ), + "driver.claude.turn.failed", + ); return; } @@ -870,21 +874,16 @@ export class ClaudeAgentSdkMessageTranslator { MAX_CLAUDE_STRUCTURED_TERMINAL_BYTES; if (structuredOutputTooLarge) { - try { - await this.finishTurnWithTerminal( - context, - "failed", - this.#events.runError( - runId, - "claude.structured_output_too_large", - "Claude Agent SDK structured output exceeds the runtime terminal event limit.", - false, - ), - "driver.claude.turn.failed", - ); - } catch (error) { - throw new ClaudeTerminalWriteError(error); - } + await finishResult( + "failed", + this.#events.runError( + runId, + "claude.structured_output_too_large", + "Claude Agent SDK structured output exceeds the runtime terminal event limit.", + false, + ), + "driver.claude.turn.failed", + ); return; } @@ -909,36 +908,26 @@ export class ClaudeAgentSdkMessageTranslator { ? null : this.#state.resolveFinalAssistantSnapshot(runId, resultText); - try { - await this.finishTurnWithTerminal( - context, - "completed", - this.#events.runFinished(runId, finalMessage, structuredOutput?.data), - "driver.claude.turn.completed", - ); - } catch (error) { - throw new ClaudeTerminalWriteError(error); - } + await finishResult( + "completed", + this.#events.runFinished(runId, finalMessage, structuredOutput?.data), + "driver.claude.turn.completed", + ); return; } - try { - await this.finishTurnWithTerminal( - context, - "failed", - this.#events.runError( - runId, - message.subtype === "success" ? "claude.api_error" : `claude.${message.subtype}`, - message.subtype === "success" - ? message.result || "Claude Agent SDK API request failed." - : message.errors.join("\n") || "Claude Agent SDK turn failed.", - isClaudeResultRetryable(message), - claudeResultErrorDetails(message), - ), - "driver.claude.turn.failed", - ); - } catch (error) { - throw new ClaudeTerminalWriteError(error); - } + await finishResult( + "failed", + this.#events.runError( + runId, + message.subtype === "success" ? "claude.api_error" : `claude.${message.subtype}`, + message.subtype === "success" + ? message.result || "Claude Agent SDK API request failed." + : message.errors.join("\n") || "Claude Agent SDK turn failed.", + isClaudeResultRetryable(message), + claudeResultErrorDetails(message), + ), + "driver.claude.turn.failed", + ); } } diff --git a/src/runtimes/claude/agent-sdk-tool-id.ts b/src/runtimes/claude/agent-sdk-tool-id.ts index 0a6c71f..d482751 100644 --- a/src/runtimes/claude/agent-sdk-tool-id.ts +++ b/src/runtimes/claude/agent-sdk-tool-id.ts @@ -1,35 +1 @@ -import { createHash } from "node:crypto"; - -import { createDriverId } from "../../protocol/id"; - -const MAX_CLAUDE_PUBLIC_TOOL_CALL_ID_BYTES = 256; - -function claudeNativeToolCallIdKey(nativeToolCallId: string): string { - return Buffer.byteLength(nativeToolCallId, "utf8") <= MAX_CLAUDE_PUBLIC_TOOL_CALL_ID_BYTES - ? nativeToolCallId - : createHash("sha256").update(nativeToolCallId).digest("hex"); -} - -export class ClaudePublicToolCallIdState { - readonly #publicIds = new Map(); - - publicId(nativeToolCallId: string): string { - if (Buffer.byteLength(nativeToolCallId, "utf8") <= MAX_CLAUDE_PUBLIC_TOOL_CALL_ID_BYTES) { - return nativeToolCallId; - } - - const key = claudeNativeToolCallIdKey(nativeToolCallId); - const existing = this.#publicIds.get(key); - if (existing !== undefined) { - return existing; - } - - const publicId = createDriverId(); - this.#publicIds.set(key, publicId); - return publicId; - } - - reset(): void { - this.#publicIds.clear(); - } -} +export { RuntimePublicIdState as ClaudePublicToolCallIdState } from "../runtime-public-id-state"; diff --git a/src/runtimes/openai/app-server-client.ts b/src/runtimes/openai/app-server-client.ts index b35281c..7d1a050 100644 --- a/src/runtimes/openai/app-server-client.ts +++ b/src/runtimes/openai/app-server-client.ts @@ -39,7 +39,7 @@ import type { ServerNotificationParams, } from "./app-server-protocol"; import { - CLIENT_REQUEST_RESULT_PARSERS, + CLIENT_RESULT_SCHEMAS, isServerNotificationMethod, isServerRequestMethod, parseServerNotification, @@ -501,7 +501,7 @@ export class OpenAiAppServerClient { params: ClientRequestParams[M], signal?: AbortSignal, ): Promise { - return this.#request(method, params, CLIENT_REQUEST_RESULT_PARSERS[method], null, signal); + return this.#request(method, params, CLIENT_RESULT_SCHEMAS[method].parse, null, signal); } async requestAtWireBarrier( @@ -513,7 +513,7 @@ export class OpenAiAppServerClient { }, signal?: AbortSignal, ): Promise { - return this.#request(method, params, CLIENT_REQUEST_RESULT_PARSERS[method], handlers, signal); + return this.#request(method, params, CLIENT_RESULT_SCHEMAS[method].parse, handlers, signal); } async #request( @@ -584,9 +584,11 @@ export class OpenAiAppServerClient { // Resuming stdout schedules the next buffered chunk on a later event-loop // turn. A microtask-only check can mistake that backpressure handoff for // an empty queue and return before the resumed notifications are admitted. - await new Promise((resolve) => { - setImmediate(resolve); - }); + if (!this.#stopRequested) { + await new Promise((resolve) => { + setImmediate(resolve); + }); + } if (tail === this.#serverMessageQueue) { if (this.#fatalError !== null) { diff --git a/src/runtimes/openai/app-server-event-state.ts b/src/runtimes/openai/app-server-event-state.ts index 7ede657..7bb33f2 100644 --- a/src/runtimes/openai/app-server-event-state.ts +++ b/src/runtimes/openai/app-server-event-state.ts @@ -1,14 +1,15 @@ -import { createHash } from "node:crypto"; - import type { DriverEventInput } from "../../protocol/events"; -import { createDriverId, type MessageId } from "../../protocol/id"; +import type { MessageId } from "../../protocol/id"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; import type { ProtocolError } from "../../contract"; +import { RuntimePublicIdState } from "../runtime-public-id-state"; import { RuntimeAssistantMessageIdIndex } from "../runtime-turn-transcript"; import { toOpenAiSessionUsageSummary } from "./app-server-event-mapping"; import { readRecord } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; +export { RuntimePublicIdState as OpenAiPublicIdState } from "../runtime-public-id-state"; + export type OpenAiEventPush = ( context: AgentDriverContext, reason: string, @@ -30,7 +31,6 @@ const usageKeys = [ "reasoningOutputTokens", "totalTokens", ] as const; -const MAX_OPENAI_PUBLIC_NATIVE_ID_BYTES = 256; export const MAX_OPENAI_DURABLE_EVENT_BYTES = 1_020 * 1_024; export function assertOpenAiDurableEventFits(event: DriverEventInput, subject: string): void { @@ -43,36 +43,6 @@ export function assertOpenAiDurableEventFits(event: DriverEventInput, subject: s } } -function openAiNativeIdKey(nativeId: string): string { - return Buffer.byteLength(nativeId, "utf8") <= MAX_OPENAI_PUBLIC_NATIVE_ID_BYTES - ? nativeId - : createHash("sha256").update(nativeId).digest("hex"); -} - -export class OpenAiPublicIdState { - readonly #publicIds = new Map(); - - publicId(nativeId: string, namespace = "id"): string { - if (Buffer.byteLength(nativeId, "utf8") <= MAX_OPENAI_PUBLIC_NATIVE_ID_BYTES) { - return nativeId; - } - - const key = `${namespace}:${openAiNativeIdKey(nativeId)}`; - const existing = this.#publicIds.get(key); - if (existing !== undefined) { - return existing; - } - - const publicId = createDriverId(); - this.#publicIds.set(key, publicId); - return publicId; - } - - reset(): void { - this.#publicIds.clear(); - } -} - type OpenAiTokenUsage = Partial>; function readUsage(value: JsonObject | null): OpenAiTokenUsage { @@ -535,7 +505,7 @@ export class OpenAiMessageState { export class OpenAiItemState { readonly #completed = new Set(); - readonly #ids = new OpenAiPublicIdState(); + readonly #ids = new RuntimePublicIdState(); isCompleted(itemId: string): boolean { return this.#completed.has(itemId); diff --git a/src/runtimes/openai/app-server-protocol-client-schemas.ts b/src/runtimes/openai/app-server-protocol-client-schemas.ts index 3de3e08..7b5325b 100644 --- a/src/runtimes/openai/app-server-protocol-client-schemas.ts +++ b/src/runtimes/openai/app-server-protocol-client-schemas.ts @@ -6,14 +6,7 @@ import threadInjectItemsResponseJsonSchema from "./generated-json-schema/ThreadI import threadResumeResponseJsonSchema from "./generated-json-schema/ThreadResumeResponse.json" with { type: "json" }; import threadStartResponseJsonSchema from "./generated-json-schema/ThreadStartResponse.json" with { type: "json" }; import turnStartResponseJsonSchema from "./generated-json-schema/TurnStartResponse.json" with { type: "json" }; -import type { - InitializeResponse, - ThreadBackgroundTerminalsCleanResponse, - ThreadInjectItemsResponse, - ThreadResumeResponse, - ThreadStartResponse, - TurnStartResponse, -} from "./app-server-protocol-types"; +import type { ClientRequestMethod, ClientRequestResult } from "./app-server-protocol-types"; import { validateTurnStatusError } from "./app-server-turn-validation"; function fromGeneratedJsonSchema(schema: unknown): z.ZodType { @@ -37,33 +30,28 @@ export const jsonRpcResponseSchema = z.union([ }), ]); -export const initializeResponseSchema = fromGeneratedJsonSchema( - initializeResponseJsonSchema, -); -export const threadBackgroundTerminalsCleanResponseSchema = - fromGeneratedJsonSchema( +export const CLIENT_RESULT_SCHEMAS: { + readonly [Method in ClientRequestMethod]: z.ZodType; +} = { + initialize: fromGeneratedJsonSchema(initializeResponseJsonSchema), + "thread/backgroundTerminals/clean": fromGeneratedJsonSchema( threadBackgroundTerminalsCleanResponseJsonSchema, - ); -export const threadInjectItemsResponseSchema = fromGeneratedJsonSchema( - threadInjectItemsResponseJsonSchema, -); -export const threadResumeResponseSchema = fromGeneratedJsonSchema( - threadResumeResponseJsonSchema, -); -export const threadStartResponseSchema = fromGeneratedJsonSchema( - threadStartResponseJsonSchema, -); -export const turnStartResponseSchema = fromGeneratedJsonSchema( - turnStartResponseJsonSchema, -).superRefine((response, context) => { - const message = validateTurnStatusError(response.turn, false); + ), + "thread/inject_items": fromGeneratedJsonSchema(threadInjectItemsResponseJsonSchema), + "thread/resume": fromGeneratedJsonSchema(threadResumeResponseJsonSchema), + "thread/start": fromGeneratedJsonSchema(threadStartResponseJsonSchema), + "turn/start": fromGeneratedJsonSchema( + turnStartResponseJsonSchema, + ).superRefine((response, context) => { + const message = validateTurnStatusError(response.turn, false); - if (message !== null) { - context.addIssue({ - code: "custom", - input: response.turn, - message, - path: ["turn", "error"], - }); - } -}); + if (message !== null) { + context.addIssue({ + code: "custom", + input: response.turn, + message, + path: ["turn", "error"], + }); + } + }), +}; diff --git a/src/runtimes/openai/app-server-protocol-client.ts b/src/runtimes/openai/app-server-protocol-client.ts deleted file mode 100644 index bcf516b..0000000 --- a/src/runtimes/openai/app-server-protocol-client.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - initializeResponseSchema, - threadBackgroundTerminalsCleanResponseSchema, - threadInjectItemsResponseSchema, - threadResumeResponseSchema, - threadStartResponseSchema, - turnStartResponseSchema, -} from "./app-server-protocol-client-schemas"; -import type { ClientRequestMethod, ClientRequestResult } from "./app-server-protocol-types"; - -export const CLIENT_REQUEST_RESULT_PARSERS: { - [Method in ClientRequestMethod]: (value: unknown) => ClientRequestResult[Method]; -} = { - initialize: (value) => initializeResponseSchema.parse(value), - "thread/backgroundTerminals/clean": (value) => - threadBackgroundTerminalsCleanResponseSchema.parse(value), - "thread/inject_items": (value) => threadInjectItemsResponseSchema.parse(value), - "thread/resume": (value) => threadResumeResponseSchema.parse(value), - "thread/start": (value) => threadStartResponseSchema.parse(value), - "turn/start": (value) => turnStartResponseSchema.parse(value), -}; diff --git a/src/runtimes/openai/app-server-protocol-server.ts b/src/runtimes/openai/app-server-protocol-server.ts index a7952a5..21c10ab 100644 --- a/src/runtimes/openai/app-server-protocol-server.ts +++ b/src/runtimes/openai/app-server-protocol-server.ts @@ -12,11 +12,6 @@ import type { } from "./app-server-protocol-types"; import { validateTurnStatusError } from "./app-server-turn-validation"; -interface MethodDispatcher { - has(method: string): method is Method; - parse(method: Method, value: unknown): JsonObject; -} - function expectObject(value: unknown, label: string): JsonObject { if (!isRecord(value)) { throw new TypeError(`${label} must be an object.`); @@ -45,7 +40,7 @@ function branchMethod(branch: JsonObject): string { return variants[0]!; } -function createMethodDispatcher(value: unknown): MethodDispatcher { +function createMethodSchemas(value: unknown): ReadonlyMap> { const root = expectObject(value, "Root method schema"); const branches = root["oneOf"]; @@ -80,35 +75,29 @@ function createMethodDispatcher(value: unknown): MethodDi schemas.set(method, schema); } - return { - has: (method: string): method is Method => schemas.has(method), - parse: (method, input) => schemas.get(method)!.parse(input), - }; + return schemas; } -const serverNotificationDispatcher = createMethodDispatcher( - serverNotificationJsonSchema, -); -const serverRequestDispatcher = - createMethodDispatcher(serverRequestJsonSchema); +const serverNotificationSchemas = createMethodSchemas(serverNotificationJsonSchema); +const serverRequestSchemas = createMethodSchemas(serverRequestJsonSchema); export function isServerNotificationMethod(method: string): method is ServerNotificationMethod { - return serverNotificationDispatcher.has(method); + return serverNotificationSchemas.has(method); } export function isServerRequestMethod(method: string): method is ServerRequestMethod { - return serverRequestDispatcher.has(method); + return serverRequestSchemas.has(method); } export function parseServerNotification(value: unknown): ParsedServerNotification | null { const envelope = isRecord(value) ? value : null; const method = envelope?.["method"]; - if (typeof method !== "string" || !serverNotificationDispatcher.has(method)) { + if (typeof method !== "string" || !isServerNotificationMethod(method)) { return null; } - const parsed = serverNotificationDispatcher.parse(method, value); + const parsed = serverNotificationSchemas.get(method)!.parse(value); const params = parsed["params"]; if (!isRecord(params)) { @@ -137,11 +126,11 @@ export function parseServerRequest(value: unknown): ParsedServerRequest | null { const envelope = isRecord(value) ? value : null; const method = envelope?.["method"]; - if (typeof method !== "string" || !serverRequestDispatcher.has(method)) { + if (typeof method !== "string" || !isServerRequestMethod(method)) { return null; } - const parsed = serverRequestDispatcher.parse(method, value); + const parsed = serverRequestSchemas.get(method)!.parse(value); const id = parsed["id"]; const params = parsed["params"]; diff --git a/src/runtimes/openai/app-server-protocol.ts b/src/runtimes/openai/app-server-protocol.ts index d155593..13ff77a 100644 --- a/src/runtimes/openai/app-server-protocol.ts +++ b/src/runtimes/openai/app-server-protocol.ts @@ -1,3 +1,3 @@ -export * from "./app-server-protocol-client"; +export { CLIENT_RESULT_SCHEMAS } from "./app-server-protocol-client-schemas"; export * from "./app-server-protocol-server"; export * from "./app-server-protocol-types"; diff --git a/src/runtimes/openai/auth-state.ts b/src/runtimes/openai/auth-state.ts index 13c73ae..a83a777 100644 --- a/src/runtimes/openai/auth-state.ts +++ b/src/runtimes/openai/auth-state.ts @@ -1,9 +1,8 @@ -import { randomUUID } from "node:crypto"; -import { mkdir, open, readFile, rename, unlink } from "node:fs/promises"; import { join } from "node:path"; import type { JsonObject, JsonValue } from "../../protocol/json"; import { isJsonObject } from "../../protocol/json"; +import { writeFileAtomicallyAtPath } from "../atomic-file"; import { mergeProviderOptions } from "../provider-options"; interface OpenAiApiKeyAuthStateInput { env: NodeJS.ProcessEnv; @@ -118,43 +117,6 @@ function stringifyToml(object: Record): string { return `${lines.join("\n")}\n`; } -async function writeFileIfChanged(path: string, contents: string): Promise { - const existing = await readFile(path, "utf8").catch(() => null); - - if (existing === contents) { - return false; - } - - await writeFileAtomically(path, contents); - return true; -} - -async function writeFileAtomically(path: string, contents: string, mode = 0o666): Promise { - const temporaryPath = `${path}.${randomUUID()}.tmp`; - let temporaryCreated = false; - - try { - const temporary = await open(temporaryPath, "wx", mode); - temporaryCreated = true; - try { - await temporary.writeFile(contents, "utf8"); - await temporary.sync(); - } finally { - await temporary.close(); - } - await rename(temporaryPath, path); - temporaryCreated = false; - } finally { - if (temporaryCreated) { - await unlink(temporaryPath).catch((error: unknown) => { - if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) { - throw error; - } - }); - } - } -} - export async function materializeOpenAiApiKeyAuthState( input: OpenAiApiKeyAuthStateInput, ): Promise { @@ -169,8 +131,7 @@ export async function materializeOpenAiApiKeyAuthState( }; } - await mkdir(input.runtimeHome, { recursive: true }); - await writeFileAtomically( + await writeFileAtomicallyAtPath( authJsonPath, `${JSON.stringify( { @@ -182,7 +143,7 @@ export async function materializeOpenAiApiKeyAuthState( null, 2, )}\n`, - 0o600, + { mode: 0o600 }, ); return { @@ -233,8 +194,10 @@ export async function materializeOpenAiModelProviderConfig( const config = mergeProviderOptions(generatedConfig, input.providerOptions ?? {}); - await mkdir(input.runtimeHome, { recursive: true }); - const written = await writeFileIfChanged(configTomlPath, stringifyToml(config)); + const written = await writeFileAtomicallyAtPath(configTomlPath, stringifyToml(config), { + mode: 0o666, + skipIfUnchanged: true, + }); return { configTomlPath, diff --git a/src/runtimes/provider-registry.ts b/src/runtimes/provider-registry.ts index f4896b2..68a999f 100644 --- a/src/runtimes/provider-registry.ts +++ b/src/runtimes/provider-registry.ts @@ -13,7 +13,7 @@ export interface AgentDriverProviderDescriptor { readonly runtime: DriverRuntime; } -const TEXT_TOOL_CAPABILITIES = [ +const PROVIDER_CAPABILITIES = [ { id: "custom_tool_execute", status: "unsupported", version: 1 }, { id: "file_change", status: "supported", version: 1 }, { id: "input_start", status: "supported", version: 1 }, @@ -25,35 +25,25 @@ const TEXT_TOOL_CAPABILITIES = [ { id: "turn_cancel", status: "supported", version: 1 }, { id: "usage", status: "supported", version: 1 }, { id: "visible_activity", status: "supported", version: 1 }, + { id: "native_resume", status: "supported", version: 1 }, + { id: "thinking_stream", status: "supported", version: 1 }, ] as const satisfies readonly DriverCapability[]; const PROVIDERS = [ { - capabilities: [ - ...TEXT_TOOL_CAPABILITIES, - { id: "native_resume", status: "supported", version: 1 }, - { id: "thinking_stream", status: "supported", version: 1 }, - ], + capabilities: PROVIDER_CAPABILITIES, createBackend: (payload) => new OpenAiAppServerDriverBackend(payload), id: "openai-app-server", runtime: "openai-runtime", }, { - capabilities: [ - ...TEXT_TOOL_CAPABILITIES, - { id: "native_resume", status: "supported", version: 1 }, - { id: "thinking_stream", status: "supported", version: 1 }, - ], + capabilities: PROVIDER_CAPABILITIES, createBackend: (payload) => new ClaudeAgentSdkDriverBackend(payload), id: "claude-agent-sdk", runtime: "claude-agent-sdk", }, { - capabilities: [ - ...TEXT_TOOL_CAPABILITIES, - { id: "native_resume", status: "supported", version: 1 }, - { id: "thinking_stream", status: "supported", version: 1 }, - ], + capabilities: PROVIDER_CAPABILITIES, createBackend: (payload) => new AcpDriverBackend(payload), id: "acp-fallback", runtime: "acp-fallback", @@ -72,19 +62,11 @@ export function createAgentDriverProviderCapabilities(input: { permissionRequestStatus: DriverCapability["status"]; provider: AgentDriverProviderDescriptor; }): readonly DriverCapability[] { - const capabilitiesById = new Map(); - - for (const capability of input.provider.capabilities) { - capabilitiesById.set(capability.id, capability); - } - - capabilitiesById.set("permission_request", { - id: "permission_request", - status: input.permissionRequestStatus, - version: 1, - }); - - return [...capabilitiesById.values()]; + return input.provider.capabilities.map((capability) => + capability.id === "permission_request" + ? { ...capability, status: input.permissionRequestStatus } + : capability, + ); } function resolveProviderForStartInput(input: DriverStartInput): AgentDriverProviderDescriptor { diff --git a/src/runtimes/runtime-public-id-state.ts b/src/runtimes/runtime-public-id-state.ts new file mode 100644 index 0000000..9118613 --- /dev/null +++ b/src/runtimes/runtime-public-id-state.ts @@ -0,0 +1,29 @@ +import { createHash } from "node:crypto"; + +import { createDriverId } from "../protocol/id"; + +const MAX_PUBLIC_NATIVE_ID_BYTES = 256; + +export class RuntimePublicIdState { + readonly #publicIds = new Map(); + + publicId(nativeId: string, namespace = "id"): string { + if (Buffer.byteLength(nativeId, "utf8") <= MAX_PUBLIC_NATIVE_ID_BYTES) { + return nativeId; + } + + const key = `${namespace}:${createHash("sha256").update(nativeId).digest("hex")}`; + const existing = this.#publicIds.get(key); + if (existing !== undefined) { + return existing; + } + + const publicId = createDriverId(); + this.#publicIds.set(key, publicId); + return publicId; + } + + reset(): void { + this.#publicIds.clear(); + } +} diff --git a/src/runtimes/skill-bootstrap.ts b/src/runtimes/skill-bootstrap.ts index 2aab8a8..eae650d 100644 --- a/src/runtimes/skill-bootstrap.ts +++ b/src/runtimes/skill-bootstrap.ts @@ -14,8 +14,9 @@ import { directoryEntryPath, ensureAbsoluteRealDirectory, ensureRealDirectoryAt, + hasErrorCode, openAbsoluteRealDirectory, - openRealDirectory, + openOptionalRealDirectory, readDirectoryEntriesBounded, writeFileAtomically, } from "./atomic-file"; @@ -41,21 +42,6 @@ function getSkillCatalogRoot(execution: DriverExecutionInput): string { return join(execution.session.sharedRootPath, ".mosoo", "skills"); } -function hasErrorCode(error: unknown, code: string): boolean { - return typeof error === "object" && error !== null && "code" in error && error.code === code; -} - -async function openOptionalRealDirectory(path: string, label: string): Promise { - try { - return await openRealDirectory(path, label); - } catch (error) { - if (hasErrorCode(error, "ENOENT")) { - return null; - } - throw error; - } -} - async function openOptionalAbsoluteRealDirectory( path: string, label: string, @@ -187,72 +173,51 @@ export async function exposeNativeSkillAliases( return []; } + await using aliasDirectory = aliasRoot.directory; const retainedAliases = new Set(); - let result: string[] | null = null; - let operationError: unknown = null; - try { - const entries = await readDirectoryEntriesBounded( - aliasRoot.directory, - "Native skill alias root", - MAX_NATIVE_SKILL_ALIAS_ENTRIES, - signal, - ); - for (const entry of entries) { - signal.throwIfAborted(); - const aliasPath = directoryEntryPath(aliasRoot.directory, entry.name); - const skill = desired.get(entry.name); - - if (entry.isSymbolicLink()) { - const target = resolve(aliasRoot.path, await readlink(aliasPath)); - if (skill !== undefined && target === resolve(skill.mountPath)) { - retainedAliases.add(entry.name); - continue; - } - if (isManagedNativeSkillAliasTarget(execution.session.sharedRootPath, target)) { - await unlink(aliasPath); - continue; - } + const entries = await readDirectoryEntriesBounded( + aliasDirectory, + "Native skill alias root", + MAX_NATIVE_SKILL_ALIAS_ENTRIES, + signal, + ); + for (const entry of entries) { + signal.throwIfAborted(); + const aliasPath = directoryEntryPath(aliasDirectory, entry.name); + const skill = desired.get(entry.name); + + if (entry.isSymbolicLink()) { + const target = resolve(aliasRoot.path, await readlink(aliasPath)); + if (skill !== undefined && target === resolve(skill.mountPath)) { + retainedAliases.add(entry.name); + continue; } - if (skill !== undefined) { - throw new Error(`Native skill alias "${entry.name}" collides with an existing path.`); + if (isManagedNativeSkillAliasTarget(execution.session.sharedRootPath, target)) { + await unlink(aliasPath); + continue; } } - - result = []; - for (const [skillName, skill] of desired) { - signal.throwIfAborted(); - result.push(join(aliasRoot.path, skillName)); - if (!retainedAliases.has(skillName)) { - await symlink( - relative(aliasRoot.path, resolve(skill.mountPath)), - directoryEntryPath(aliasRoot.directory, skillName), - "dir", - ); - } + if (skill !== undefined) { + throw new Error(`Native skill alias "${entry.name}" collides with an existing path.`); } - await aliasRoot.directory.sync(); - signal.throwIfAborted(); - await assertDirectoryIdentity(aliasRoot.directory, aliasRoot.path, "Native skill alias root"); - } catch (error) { - operationError = error; } - const closeFailures = await closeFileHandles([aliasRoot.directory]); - if (operationError !== null) { - if (closeFailures.length > 0) { - throw new AggregateError( - [operationError, ...closeFailures], - "Native skill alias update and cleanup failed.", + const result: string[] = []; + for (const [skillName, skill] of desired) { + signal.throwIfAborted(); + result.push(join(aliasRoot.path, skillName)); + if (!retainedAliases.has(skillName)) { + await symlink( + relative(aliasRoot.path, resolve(skill.mountPath)), + directoryEntryPath(aliasDirectory, skillName), + "dir", ); } - throw operationError; - } - if (closeFailures.length > 0) { - throw new AggregateError(closeFailures, "Failed to close the native skill alias root."); - } - if (result === null) { - throw new Error("Native skill aliases completed without a result."); } + await aliasDirectory.sync(); + signal.throwIfAborted(); + await assertDirectoryIdentity(aliasDirectory, aliasRoot.path, "Native skill alias root"); + return result; } @@ -330,57 +295,37 @@ async function removeSkillBootstrapArtifacts( if (sharedRootDirectory === null) { return; } - let mosooRootDirectory: FileHandle | null = null; - let skillCatalogRootDirectory: FileHandle | null = null; - let operationError: unknown = null; - - try { - mosooRootDirectory = await openOptionalRealDirectory( - directoryEntryPath(sharedRootDirectory, ".mosoo"), - "Skill bootstrap .mosoo root", - ); - if (mosooRootDirectory !== null) { - skillCatalogRootDirectory = await openOptionalRealDirectory( - directoryEntryPath(mosooRootDirectory, "skills"), - "Skill bootstrap catalog root", - ); - } - if (skillCatalogRootDirectory !== null) { - await cleanupAtomicWriteTemporaryFiles( - skillCatalogRootDirectory, - ["manifest.json", "README.md"], - signal, - ); - await unlinkManagedFile(skillCatalogRootDirectory, "manifest.json", signal); - await unlinkManagedFile(skillCatalogRootDirectory, "README.md", signal); - await skillCatalogRootDirectory.sync(); - await assertDirectoryIdentity( - skillCatalogRootDirectory, - getSkillCatalogRoot(execution), - "Skill bootstrap catalog root", - ); - } - } catch (error) { - operationError = error; - } - - const closeFailures = await closeFileHandles([ - skillCatalogRootDirectory, - mosooRootDirectory, - sharedRootDirectory, - ]); - if (operationError !== null) { - if (closeFailures.length > 0) { - throw new AggregateError( - [operationError, ...closeFailures], - "Skill bootstrap removal and cleanup failed.", - ); - } - throw operationError; + await using ownedSharedRootDirectory = sharedRootDirectory; + const mosooRootDirectory = await openOptionalRealDirectory( + directoryEntryPath(ownedSharedRootDirectory, ".mosoo"), + "Skill bootstrap .mosoo root", + ); + if (mosooRootDirectory === null) { + return; } - if (closeFailures.length > 0) { - throw new AggregateError(closeFailures, "Failed to close skill bootstrap roots."); + await using ownedMosooRootDirectory = mosooRootDirectory; + const skillCatalogRootDirectory = await openOptionalRealDirectory( + directoryEntryPath(ownedMosooRootDirectory, "skills"), + "Skill bootstrap catalog root", + ); + if (skillCatalogRootDirectory === null) { + return; } + await using ownedSkillCatalogRootDirectory = skillCatalogRootDirectory; + + await cleanupAtomicWriteTemporaryFiles( + ownedSkillCatalogRootDirectory, + ["manifest.json", "README.md"], + signal, + ); + await unlinkManagedFile(ownedSkillCatalogRootDirectory, "manifest.json", signal); + await unlinkManagedFile(ownedSkillCatalogRootDirectory, "README.md", signal); + await ownedSkillCatalogRootDirectory.sync(); + await assertDirectoryIdentity( + ownedSkillCatalogRootDirectory, + getSkillCatalogRoot(execution), + "Skill bootstrap catalog root", + ); } function getSkillCatalogManifestEntries( diff --git a/src/runtimes/skill-materialization.ts b/src/runtimes/skill-materialization.ts index e4df9b4..25ea5b2 100644 --- a/src/runtimes/skill-materialization.ts +++ b/src/runtimes/skill-materialization.ts @@ -1,6 +1,7 @@ +import { Buffer } from "node:buffer"; import { createHash, randomUUID } from "node:crypto"; import { constants } from "node:fs"; -import { lstat, mkdir, open, opendir, rename, rmdir, unlink } from "node:fs/promises"; +import { mkdir, open, opendir, rename, rmdir, unlink } from "node:fs/promises"; import type { FileHandle } from "node:fs/promises"; import { basename, dirname, join, resolve } from "node:path"; @@ -15,9 +16,13 @@ import { closeFileHandles, directoryEntryPath, ensureRealDirectoryAt, + hasErrorCode, openedDirectoryPath, openAbsoluteRealDirectory, + openOptionalRealDirectory, openRealDirectory, + openRelativeRealDirectory, + readPathStats, readDirectoryEntriesBounded, } from "./atomic-file"; @@ -95,10 +100,6 @@ function createSerialLock(): (operation: () => Promise) => Promise { const withCommitLock = createSerialLock(); const withCleanupLock = createSerialLock(); -function hasErrorCode(error: unknown, code: string): boolean { - return typeof error === "object" && error !== null && "code" in error && error.code === code; -} - function enforceSkillMountPath(sessionOrganizationPath: string, mountPath: string): string { const allowedRoot = resolve(sessionOrganizationPath, ".mosoo", "skill"); const resolvedMountPath = resolve(mountPath); @@ -110,29 +111,6 @@ function enforceSkillMountPath(sessionOrganizationPath: string, mountPath: strin return resolvedMountPath; } -async function readPathStats(path: string): Promise> | null> { - try { - return await lstat(path); - } catch (error) { - if (hasErrorCode(error, "ENOENT")) { - return null; - } - - throw error; - } -} - -async function openOptionalRealDirectory(path: string, label: string): Promise { - try { - return await openRealDirectory(path, label); - } catch (error) { - if (hasErrorCode(error, "ENOENT")) { - return null; - } - throw error; - } -} - async function ensureMaterializationRoots( sessionOrganizationPath: string, create: boolean, @@ -378,53 +356,30 @@ export async function materializeResolvedSkills( }); const operationSignal = AbortSignal.any([signal, generation.signal]); - let roots: MaterializationRoots | null = null; - let result: AgentDriverMaterializedSkill[] | null = null; - let materializationError: unknown = null; try { - roots = await ensureMaterializationRoots( + const roots = await ensureMaterializationRoots( execution.session.sharedRootPath, inputs.length > 0, operationSignal, ); if (roots === null) { - result = []; - } else { - result = await materializeSkillGeneration( - logger, - inputs, - roots, - generation.signal, - operationSignal, - ); + return []; } - } catch (error) { - materializationError = error; - } - if (latestGenerationByRoot.get(rootKey) === generation) { - latestGenerationByRoot.delete(rootKey); - } - const closeFailures = - roots === null - ? [] - : await closeFileHandles([roots.transactionDirectory, roots.mosooDirectory]); - if (materializationError !== null) { - if (closeFailures.length > 0) { - throw new AggregateError( - [materializationError, ...closeFailures], - "Skill materialization and root cleanup failed.", - ); + await using _mosooDirectory = roots.mosooDirectory; + await using _transactionDirectory = roots.transactionDirectory; + return await materializeSkillGeneration( + logger, + inputs, + roots, + generation.signal, + operationSignal, + ); + } finally { + if (latestGenerationByRoot.get(rootKey) === generation) { + latestGenerationByRoot.delete(rootKey); } - throw materializationError; - } - if (closeFailures.length > 0) { - throw new AggregateError(closeFailures, "Failed to close skill materialization roots."); - } - if (result === null) { - throw new Error("Skill materialization completed without a result."); } - return result; } async function materializeSkillGeneration( @@ -991,8 +946,9 @@ async function recoverSkillTransactions( } const owner = await openExistingMaterializationOwner(roots); let retiredPath: string | null = null; - let recoveryError: unknown = null; - try { + { + await using _ownerDirectory = owner.ownerDirectory; + await using _newDirectory = owner.newDirectory; if (await hasSkillTransactionCommitMarker(owner)) { await finishCommittedSkillTransaction(owner, roots); await retireMaterializationOwner(owner, roots); @@ -1002,21 +958,6 @@ async function recoverSkillTransactions( logger.info("driver.skill.materialization.transaction_restored", {}); } retiredPath = directoryEntryPath(roots.transactionDirectory, owner.currentName); - } catch (error) { - recoveryError = error; - } - const closeFailures = await closeFileHandles([owner.newDirectory, owner.ownerDirectory]); - if (recoveryError !== null) { - if (closeFailures.length > 0) { - throw new AggregateError( - [recoveryError, ...closeFailures], - "Skill transaction recovery and cleanup failed.", - ); - } - throw recoveryError; - } - if (closeFailures.length > 0) { - throw new AggregateError(closeFailures, "Failed to close recovered skill transaction."); } if (retiredPath !== null) { try { @@ -1071,39 +1012,6 @@ async function recoverSkillTransactions( await roots.transactionDirectory.sync(); } -async function openRelativeSkillDirectory( - root: FileHandle, - path: string, - create: boolean, - signal: AbortSignal, -): Promise { - signal.throwIfAborted(); - let directory = await openRealDirectory( - `${openedDirectoryPath(root)}/.`, - "Staged skill directory", - ); - - try { - for (const segment of path === "." ? [] : path.split("/")) { - signal.throwIfAborted(); - const next = create - ? await ensureRealDirectoryAt(directory, segment, "Staged skill directory", signal) - : await openRealDirectory(directoryEntryPath(directory, segment), "Staged skill directory"); - try { - await directory.close(); - } catch (error) { - await next.close(); - throw error; - } - directory = next; - } - return directory; - } catch (error) { - await directory.close().catch(() => {}); - throw error; - } -} - async function materializeSkillEntry( stagingDirectory: FileHandle, entry: SkillPackageEntry, @@ -1112,18 +1020,20 @@ async function materializeSkillEntry( signal.throwIfAborted(); if (entry.entryKind === "directory") { - await using _directory = await openRelativeSkillDirectory( + await using _directory = await openRelativeRealDirectory( stagingDirectory, entry.path, + "Staged skill directory", true, signal, ); return; } - await using parent = await openRelativeSkillDirectory( + await using parent = await openRelativeRealDirectory( stagingDirectory, dirname(entry.path), + "Staged skill directory", true, signal, ); @@ -1156,9 +1066,10 @@ async function syncSkillStageDirectories( (a, b) => b.split("/").length - a.split("/").length, )) { signal.throwIfAborted(); - await using handle = await openRelativeSkillDirectory( + await using handle = await openRelativeRealDirectory( stagingDirectory, directory, + "Staged skill directory", false, signal, ); @@ -1233,12 +1144,7 @@ async function downloadSkillPackage( reader.releaseLock(); } - const bytes = new Uint8Array(totalBytes); - let offset = 0; - for (const chunk of chunks) { - bytes.set(chunk, offset); - offset += chunk.byteLength; - } + const bytes = Buffer.concat(chunks, totalBytes); if (hash.digest("hex") !== skill.blobSha256) { throw new Error(`Skill blob checksum mismatch for ${skill.skillId}.`); diff --git a/src/surfaces/cma-http/request.ts b/src/surfaces/cma-http/request.ts index 43466f6..689ab41 100644 --- a/src/surfaces/cma-http/request.ts +++ b/src/surfaces/cma-http/request.ts @@ -14,6 +14,7 @@ import { CMA_ENVIRONMENT_PACKAGE_MANAGERS, createDefaultCmaEnvironmentConfig, } from "../../stores/cma-environment"; +import { readBoundedStreamBytes } from "../../utils/async"; import { CmaHttpCapabilityGapError, CmaHttpRequestError } from "./contract"; const createAgentFields = new Set(["id", "metadata", "name"]); @@ -169,67 +170,33 @@ function readRequiredRecord( export async function readCmaJsonBody(request: Request): Promise { const contentLength = request.headers.get("content-length"); + const limitError = new CmaHttpRequestError( + 413, + "CMA_REQUEST_BODY_TOO_LARGE", + `Request body exceeds ${CMA_MAX_EVENT_BYTES} UTF-8 bytes.`, + ); if (contentLength !== null && Number(contentLength) > CMA_MAX_EVENT_BYTES) { - throw new CmaHttpRequestError( - 413, - "CMA_REQUEST_BODY_TOO_LARGE", - `Request body exceeds ${CMA_MAX_EVENT_BYTES} UTF-8 bytes.`, - ); + throw limitError; } - const reader = request.body?.getReader(); - - if (!reader) { + if (!request.body) { throw new CmaHttpRequestError(400, "CMA_INVALID_JSON", "Request body must be valid JSON."); } - let body = new Uint8Array(0); - let size = 0; - try { - while (true) { - const chunk = await reader.read(); - - if (chunk.done) { - break; - } - - const nextSize = size + chunk.value.byteLength; - - if (nextSize > CMA_MAX_EVENT_BYTES) { - await reader.cancel().catch(() => undefined); - throw new CmaHttpRequestError( - 413, - "CMA_REQUEST_BODY_TOO_LARGE", - `Request body exceeds ${CMA_MAX_EVENT_BYTES} UTF-8 bytes.`, - ); - } - - if (nextSize > body.byteLength) { - const grown = new Uint8Array( - Math.min(CMA_MAX_EVENT_BYTES, Math.max(nextSize, body.byteLength * 2, 1_024)), - ); - grown.set(body); - body = grown; - } - - body.set(chunk.value, size); - size = nextSize; - } + const body = await readBoundedStreamBytes( + request.body, + CMA_MAX_EVENT_BYTES, + limitError, + request.signal, + ); + return JSON.parse(new TextDecoder().decode(body)) as unknown; } catch (error) { if (error instanceof CmaHttpRequestError) { throw error; } - throw new CmaHttpRequestError(400, "CMA_INVALID_JSON", "Request body must be valid JSON."); - } finally { - reader.releaseLock(); - } - - try { - return JSON.parse(new TextDecoder().decode(body.subarray(0, size))) as unknown; - } catch { throw new CmaHttpRequestError(400, "CMA_INVALID_JSON", "Request body must be valid JSON."); } } diff --git a/src/surfaces/cma-sdk/client.ts b/src/surfaces/cma-sdk/client.ts index 8156757..daa45fa 100644 --- a/src/surfaces/cma-sdk/client.ts +++ b/src/surfaces/cma-sdk/client.ts @@ -9,12 +9,11 @@ import type { CmaSessionEventRecord, CmaSessionRecord, } from "../../stores/cma-store"; -import { raceWithAbort } from "../../utils/async"; -import { CMA_DEFAULT_BETA_HEADER_NAME, CMA_DEFAULT_BETA_HEADER_VALUE } from "../cma-http"; +import { raceWithAbort, readBoundedStreamBytes } from "../../utils/async"; +import { CMA_DEFAULT_BETA_HEADER_NAME, CMA_DEFAULT_BETA_HEADER_VALUE } from "../cma-http/contract"; import { decodeCmaSseBytes } from "./sse-bytes-decoder"; import { CmaSdkError, - type CmaSdkClient, type CmaSdkClientOptions, type CmaSdkFetch, type CmaSdkRequestOptions, @@ -29,34 +28,17 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } -function readErrorCode(body: unknown): string { - if (!isRecord(body)) { - return "CMA_SDK_HTTP_ERROR"; - } - - const error = body["error"]; - - if (!isRecord(error)) { - return "CMA_SDK_HTTP_ERROR"; - } - +function readError( + body: unknown, + fallback: string, +): { readonly code: string; readonly message: string } { + const error = isRecord(body) && isRecord(body["error"]) ? body["error"] : {}; const code = error["code"]; - return typeof code === "string" && code.length > 0 ? code : "CMA_SDK_HTTP_ERROR"; -} - -function readErrorMessage(body: unknown, fallback: string): string { - if (!isRecord(body)) { - return fallback; - } - - const error = body["error"]; - - if (!isRecord(error)) { - return fallback; - } - const message = error["message"]; - return typeof message === "string" && message.length > 0 ? message : fallback; + return { + code: typeof code === "string" && code.length > 0 ? code : "CMA_SDK_HTTP_ERROR", + message: typeof message === "string" && message.length > 0 ? message : fallback, + }; } function readData(body: unknown): unknown { @@ -103,75 +85,25 @@ async function readResponseBody( throw responseTooLarge(maxResponseBytes); } - const reader = response.body?.getReader(); - - if (!reader) { + if (!response.body) { return null; } - let body = new Uint8Array(0); - let cancellation: Promise | undefined; - let completed = false; - let size = 0; - const cancel = () => - (cancellation ??= reader.cancel(signal.reason).then( - () => undefined, - () => undefined, - )); - const onAbort = () => void cancel(); - signal.addEventListener("abort", onAbort, { once: true }); - - try { - signal.throwIfAborted(); - - while (true) { - const chunk = await raceWithAbort(reader.read(), signal); - signal.throwIfAborted(); - - if (chunk.done) { - completed = true; - break; - } - - if (chunk.value.byteLength > maxResponseBytes - size) { - throw responseTooLarge(maxResponseBytes); - } - - const nextSize = size + chunk.value.byteLength; - - if (nextSize > body.byteLength) { - const grown = new Uint8Array( - Math.min(maxResponseBytes, Math.max(nextSize, body.byteLength * 2, 1_024)), - ); - grown.set(body); - body = grown; - } - - body.set(chunk.value, size); - size = nextSize; - } - } finally { - signal.removeEventListener("abort", onAbort); - - if (!completed) { - await cancel(); - } - - reader.releaseLock(); - } + const body = await readBoundedStreamBytes( + response.body, + maxResponseBytes, + responseTooLarge(maxResponseBytes), + signal, + ); try { - return JSON.parse(new TextDecoder().decode(body.subarray(0, size))) as unknown; + return JSON.parse(new TextDecoder().decode(body)) as unknown; } catch { return null; } } -export function createCmaSdkClient(options: CmaSdkClientOptions): CmaSdkClient { - return new CmaSdkClientCore(options); -} - -class CmaSdkClientCore implements CmaSdkClient { +export class CmaSdkClient { readonly #baseUrl: URL; readonly #fetch: CmaSdkFetch; readonly #headers: Headers; @@ -203,8 +135,7 @@ class CmaSdkClientCore implements CmaSdkClient { ): Promise { return this.#requestData( `/v1/environments/${encodeURIComponent(id)}/archive`, - { method: "POST" }, - options, + { ...options, method: "POST" }, ); } @@ -212,78 +143,63 @@ class CmaSdkClientCore implements CmaSdkClient { input: CmaCreateAgentInput, options?: CmaSdkRequestOptions, ): Promise { - return this.#requestData( - "/v1/agents", - { - body: JSON.stringify(input), - method: "POST", - }, - options, - ); + return this.#requestData("/v1/agents", { + ...options, + body: JSON.stringify(input), + method: "POST", + }); } async createEnvironment( input: CmaCreateEnvironmentInput, options?: CmaSdkRequestOptions, ): Promise { - return this.#requestData( - "/v1/environments", - { - body: JSON.stringify(input), - method: "POST", - }, - options, - ); + return this.#requestData("/v1/environments", { + ...options, + body: JSON.stringify(input), + method: "POST", + }); } async createSession( input: CmaCreateSessionInput, options?: CmaSdkRequestOptions, ): Promise { - return this.#requestData( - "/v1/sessions", - { - body: JSON.stringify(input), - method: "POST", - }, - options, - ); + return this.#requestData("/v1/sessions", { + ...options, + body: JSON.stringify(input), + method: "POST", + }); } async deleteEnvironment(id: string, options?: CmaSdkRequestOptions): Promise { - await this.#requestBody( - `/v1/environments/${encodeURIComponent(id)}`, - { method: "DELETE" }, - options, - ); + await this.#requestBody(`/v1/environments/${encodeURIComponent(id)}`, { + ...options, + method: "DELETE", + }); } async getAgent(id: string, options?: CmaSdkRequestOptions): Promise { - return this.#requestData(`/v1/agents/${encodeURIComponent(id)}`, {}, options); + return this.#requestData(`/v1/agents/${encodeURIComponent(id)}`, options); } async getEnvironment(id: string, options?: CmaSdkRequestOptions): Promise { return this.#requestData( `/v1/environments/${encodeURIComponent(id)}`, - {}, options, ); } async getSession(id: string, options?: CmaSdkRequestOptions): Promise { - return this.#requestData( - `/v1/sessions/${encodeURIComponent(id)}`, - {}, - options, - ); + return this.#requestData(`/v1/sessions/${encodeURIComponent(id)}`, options); } async listAgents(options?: CmaSdkRequestOptions): Promise { - return this.#requestData("/v1/agents", {}, options); + return this.#requestData("/v1/agents", options); } async listEnvironments(options?: CmaSdkRequestOptions): Promise { - return this.#requestData("/v1/environments", {}, options); + return this.#requestData("/v1/environments", options); } async listSessionEvents( @@ -292,7 +208,6 @@ class CmaSdkClientCore implements CmaSdkClient { ): Promise { return this.#requestData( `/v1/sessions/${encodeURIComponent(sessionId)}/events`, - {}, options, ); } @@ -304,8 +219,7 @@ class CmaSdkClientCore implements CmaSdkClient { ): Promise { return this.#requestData( `/v1/sessions/${encodeURIComponent(sessionId)}/events`, - { body: JSON.stringify(event), method: "POST" }, - options, + { ...options, body: JSON.stringify(event), method: "POST" }, ); } @@ -383,20 +297,12 @@ class CmaSdkClientCore implements CmaSdkClient { } const body = await readResponseBody(response, this.#maxResponseBytes, signal); - throw new CmaSdkError( - response.status, - readErrorCode(body), - readErrorMessage(body, `CMA request failed with status ${response.status}.`), - body, - ); + const error = readError(body, `CMA request failed with status ${response.status}.`); + throw new CmaSdkError(response.status, error.code, error.message, body); } - async #requestBody( - path: string, - init: RequestInit, - options: CmaSdkRequestOptions | undefined, - ): Promise { - const request = this.#startRequest(options?.signal); + async #requestBody(path: string, init: RequestInit = {}): Promise { + const request = this.#startRequest(init.signal ?? undefined); try { const response = await this.#request(path, init, request.signal); @@ -406,12 +312,8 @@ class CmaSdkClientCore implements CmaSdkClient { } } - async #requestData( - path: string, - init: RequestInit, - options: CmaSdkRequestOptions | undefined, - ): Promise { - return readData(await this.#requestBody(path, init, options)) as T; + async #requestData(path: string, init: RequestInit = {}): Promise { + return readData(await this.#requestBody(path, init)) as T; } #combineSignals(...signals: readonly (AbortSignal | undefined)[]): AbortSignal { diff --git a/src/surfaces/cma-sdk/index.ts b/src/surfaces/cma-sdk/index.ts index d7d232f..64f7663 100644 --- a/src/surfaces/cma-sdk/index.ts +++ b/src/surfaces/cma-sdk/index.ts @@ -1,8 +1,7 @@ -export { createCmaSdkClient } from "./client"; +export { CmaSdkClient } from "./client"; export { CmaSdkError, type CmaSdkBetaHeader, - type CmaSdkClient, type CmaSdkClientOptions, type CmaSdkFetch, type CmaSdkRequestOptions, diff --git a/src/surfaces/cma-sdk/types.ts b/src/surfaces/cma-sdk/types.ts index d9ae8d7..38c7d3a 100644 --- a/src/surfaces/cma-sdk/types.ts +++ b/src/surfaces/cma-sdk/types.ts @@ -1,14 +1,5 @@ -import type { CmaInboundEvent } from "../../projections/cma"; import type { RuntimeCommand, RuntimeCommandResult } from "../../runtime-command"; -import type { - CmaAgentRecord, - CmaCreateAgentInput, - CmaCreateEnvironmentInput, - CmaCreateSessionInput, - CmaEnvironmentRecord, - CmaSessionEventRecord, - CmaSessionRecord, -} from "../../stores/cma-store"; +import type { CmaSessionEventRecord } from "../../stores/cma-store"; export type CmaSdkFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; @@ -42,38 +33,6 @@ export interface CmaSessionEventDispatchRecord { readonly status: "accepted"; } -export interface CmaSdkClient { - archiveEnvironment(id: string, options?: CmaSdkRequestOptions): Promise; - createAgent(input: CmaCreateAgentInput, options?: CmaSdkRequestOptions): Promise; - createEnvironment( - input: CmaCreateEnvironmentInput, - options?: CmaSdkRequestOptions, - ): Promise; - createSession( - input: CmaCreateSessionInput, - options?: CmaSdkRequestOptions, - ): Promise; - deleteEnvironment(id: string, options?: CmaSdkRequestOptions): Promise; - getAgent(id: string, options?: CmaSdkRequestOptions): Promise; - getEnvironment(id: string, options?: CmaSdkRequestOptions): Promise; - getSession(id: string, options?: CmaSdkRequestOptions): Promise; - listAgents(options?: CmaSdkRequestOptions): Promise; - listEnvironments(options?: CmaSdkRequestOptions): Promise; - listSessionEvents( - sessionId: string, - options?: CmaSdkRequestOptions, - ): Promise; - sendSessionEvent( - sessionId: string, - event: CmaInboundEvent, - options?: CmaSdkRequestOptions, - ): Promise; - streamSessionEvents( - sessionId: string, - options?: CmaSdkStreamOptions, - ): AsyncIterable; -} - export class CmaSdkError extends Error { readonly body: unknown; readonly code: string; diff --git a/src/utils/async.ts b/src/utils/async.ts index 15aca83..50f9c27 100644 --- a/src/utils/async.ts +++ b/src/utils/async.ts @@ -119,6 +119,67 @@ export async function raceWithAbort(promise: Promise, signal?: AbortSignal } } +export async function readBoundedStreamBytes( + body: ReadableStream, + maxBytes: number, + limitError: Error, + signal?: AbortSignal, +): Promise { + const reader = body.getReader(); + let bytes = new Uint8Array(0); + let cancellation: Promise | undefined; + let completed = false; + let size = 0; + const cancel = () => + (cancellation ??= reader.cancel(signal?.reason).then( + () => undefined, + () => undefined, + )); + const onAbort = () => void cancel(); + signal?.addEventListener("abort", onAbort, { once: true }); + + try { + signal?.throwIfAborted(); + + while (true) { + const chunk = await raceWithAbort(reader.read(), signal); + signal?.throwIfAborted(); + + if (chunk.done) { + completed = true; + break; + } + + if (chunk.value.byteLength > maxBytes - size) { + throw limitError; + } + + const nextSize = size + chunk.value.byteLength; + + if (nextSize > bytes.byteLength) { + const grown = new Uint8Array( + Math.min(maxBytes, Math.max(nextSize, bytes.byteLength * 2, 1_024)), + ); + grown.set(bytes); + bytes = grown; + } + + bytes.set(chunk.value, size); + size = nextSize; + } + } finally { + signal?.removeEventListener("abort", onAbort); + + if (!completed) { + await cancel(); + } + + reader.releaseLock(); + } + + return bytes.subarray(0, size); +} + export function settlePromiseWithTimeout( promise: Promise, options: PromiseTimeoutOptions, diff --git a/tests/acp-agent-process.test.ts b/tests/acp-agent-process.test.ts index 5916228..9659b07 100644 --- a/tests/acp-agent-process.test.ts +++ b/tests/acp-agent-process.test.ts @@ -6,7 +6,7 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; import { startAcpAgentProcess, @@ -19,22 +19,17 @@ import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { driverBootPayload, driverStartInput } from "./driver-boot-payload-fixture"; function createHarness() { - const logger = createBufferedSinkLogger({ - level: "debug", - service: "acp-agent-process-test", - sink: async () => {}, - }); const context = createAgentDriverContext({ eventSink: { currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload: driverStartInput, permission: { request: async () => "reject_once" }, }); - return { context, logger }; + return { context }; } function createPayload(root: string) { @@ -185,7 +180,6 @@ describe("ACP agent process lifecycle", () => { child.kill("SIGKILL"); } await rm(root, { force: true, recursive: true }); - await harness.logger.destroy(); } }); @@ -203,7 +197,6 @@ describe("ACP agent process lifecycle", () => { expect(process.child.signalCode).toBe(exitSignal); } finally { await process.dispose(); - await harness.logger.destroy(); } }); @@ -228,7 +221,6 @@ describe("ACP agent process lifecycle", () => { } finally { cleanup.resolve(); await process.dispose(); - await harness.logger.destroy(); } }); @@ -281,7 +273,6 @@ describe("ACP agent process lifecycle", () => { child.kill("SIGKILL"); } await rm(root, { force: true, recursive: true }); - await harness.logger.destroy(); } }, ); @@ -321,7 +312,6 @@ describe("ACP agent process lifecycle", () => { } finally { cleanup.resolve(); await agent.dispose(); - await harness.logger.destroy(); } }, ); @@ -423,7 +413,6 @@ setInterval(() => {}, 1_000); } } await rm(root, { force: true, recursive: true }); - await harness.logger.destroy(); } }, 7_000); @@ -503,7 +492,6 @@ setInterval(() => {}, 1_000); } } await rm(root, { force: true, recursive: true }); - await harness.logger.destroy(); } }, 7_000); @@ -567,7 +555,6 @@ setInterval(() => {}, 1_000); } } await rm(root, { force: true, recursive: true }); - await harness.logger.destroy(); } }, 5_000); }); diff --git a/tests/acp-configuration.test.ts b/tests/acp-configuration.test.ts index b87d5e3..bf27a83 100644 --- a/tests/acp-configuration.test.ts +++ b/tests/acp-configuration.test.ts @@ -4,7 +4,7 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import { ACP_PROTOCOL_VERSION, appendOpenCodeInstruction, @@ -309,11 +309,6 @@ describe("ACP runtime configuration", () => { }, }, }; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "acp-configuration-test", - sink: async () => {}, - }); const backend = new AcpDriverBackend(payload); const context = createAgentDriverContext({ eventSink: { @@ -321,7 +316,7 @@ describe("ACP runtime configuration", () => { currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload, permission: { request: async () => "reject_once", @@ -333,7 +328,6 @@ describe("ACP runtime configuration", () => { "ACP fallback requires a host integration snapshot.", ); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); diff --git a/tests/acp-driver-backend.test.ts b/tests/acp-driver-backend.test.ts index 0dee8a6..ce97b7f 100644 --- a/tests/acp-driver-backend.test.ts +++ b/tests/acp-driver-backend.test.ts @@ -10,7 +10,7 @@ import { PermissionEventDeliveryError, } from "../src/core/driver-permission-broker"; import type { DriverRuntimeEventPort } from "../src/core/driver-runtime-io"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { AgentDriverPermissionPort } from "../src/host-ports"; import type { DriverBootPayload } from "../src/protocol/boot"; import { createDriverHostIntegrationSnapshotFromBootExecution } from "../src/protocol/host-integration"; @@ -505,11 +505,7 @@ async function createHarness( runtimeTransport: "acp-fallback", } satisfies DriverBootPayload; const payload = createDriverStartInputFromBootPayload(boot); - const logger = createBufferedSinkLogger({ - level: "debug", - service: "acp-driver-backend-test", - sink: async () => {}, - }); + const logger = createDisabledLogger(); let acceptedSeq = 0; const lifecycleFailures: Error[] = []; const lifecycleFailure = Promise.withResolvers(); @@ -610,7 +606,6 @@ async function createHarness( async destroy() { block?.release.reject(new Error("test cleanup")); await backend.stop(context, "test cleanup", new AbortController().signal).catch(() => {}); - await logger.destroy(); await rm(root, { force: true, recursive: true }); }, failNext(kind: string) { @@ -663,11 +658,7 @@ describe("ACP driver backend lifecycle", () => { runtimeTransport: "acp-fallback", } satisfies DriverBootPayload; const payload = createDriverStartInputFromBootPayload(boot); - const logger = createBufferedSinkLogger({ - level: "debug", - service: "acp-root-init-failure-test", - sink: async () => {}, - }); + const logger = createDisabledLogger(); let hostSnapshots = 0; let materializations = 0; const context = createAgentDriverContext({ @@ -709,7 +700,6 @@ describe("ACP driver backend lifecycle", () => { ).resolves.toEqual([undefined, undefined]); } finally { await backend.stop(context, "test cleanup", new AbortController().signal).catch(() => {}); - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); diff --git a/tests/acp-event-translator-lifecycle.test.ts b/tests/acp-event-translator-lifecycle.test.ts index 69a1ce6..a756eaf 100644 --- a/tests/acp-event-translator-lifecycle.test.ts +++ b/tests/acp-event-translator-lifecycle.test.ts @@ -8,7 +8,7 @@ import { import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; import { AcpAssistantTranscriptState } from "../src/runtimes/acp/acp-assistant-transcript-state"; @@ -116,11 +116,7 @@ describe("ACP runtime event translation", () => { const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); const canonicalEvents: DriverEventInput[] = []; let cmaRecordCount = 0; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "acp-terminal-boundary-test", - sink: async () => {}, - }); + const logger = createDisabledLogger(); let sequence = 0; const context = createAgentDriverContext({ eventSink: { @@ -151,16 +147,12 @@ describe("ACP runtime event translation", () => { permission: { request: async () => "reject_once" }, }); - try { - await new DriverEventPublisher("acp-fallback", () => "native-session-1").pushTerminal( - context, - "driver.acp.prompt.failed", - closures, - terminal, - ); - } finally { - await logger.destroy(); - } + await new DriverEventPublisher("acp-fallback", () => "native-session-1").pushTerminal( + context, + "driver.acp.prompt.failed", + closures, + terminal, + ); const boundedMessage = "ACP failure exceeded durable event capacity (originalMessageUtf8Bytes=1100000)."; diff --git a/tests/acp-file-system.test.ts b/tests/acp-file-system.test.ts index c21bf41..26d4042 100644 --- a/tests/acp-file-system.test.ts +++ b/tests/acp-file-system.test.ts @@ -21,8 +21,7 @@ import { join } from "node:path"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; -import { createBufferedSinkLogger } from "../src/observability"; -import type { Logger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; import { AcpFileSystem } from "../src/runtimes/acp/acp-file-system"; @@ -47,38 +46,26 @@ afterEach(async () => { pathScopes.clear(); }); -function createContext(events: DriverEventInput[]): { - context: AgentDriverContext; - logger: Logger; -} { - const logger = createBufferedSinkLogger({ - level: "debug", - service: "acp-file-system-test", - sink: async () => {}, - }); - - return { - context: createAgentDriverContext({ - eventSink: { - currentRunId: () => null, - pushEvents: async (input) => { - events.push(...input.events); - return { - accepted: input.events.map((event, index) => ({ - seq: index + 1, - type: event.kind, - })), - }; - }, +function createContext(events: DriverEventInput[]): AgentDriverContext { + return createAgentDriverContext({ + eventSink: { + currentRunId: () => null, + pushEvents: async (input) => { + events.push(...input.events); + return { + accepted: input.events.map((event, index) => ({ + seq: index + 1, + type: event.kind, + })), + }; }, - logger, - payload: driverStartInput, - permission: { - request: async () => "reject_once", - }, - }), - logger, - }; + }, + logger: createDisabledLogger(), + payload: driverStartInput, + permission: { + request: async () => "reject_once", + }, + }); } describe("ACP file system bridge", () => { @@ -120,7 +107,7 @@ describe("ACP file system bridge", () => { const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-")); const path = join(root, "nested", "note.txt"); const events: DriverEventInput[] = []; - const { context, logger } = createContext(events); + const context = createContext(events); try { const fileSystem = createFileSystem(root); @@ -145,7 +132,6 @@ describe("ACP file system bridge", () => { }); expect(isDriverId(events[0]?.sourceEventId)).toBe(true); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -203,7 +189,7 @@ describe("ACP file system bridge", () => { const requestedRead = join(root, "read.txt"); const requestedWrite = join(root, "write.txt"); const events: DriverEventInput[] = []; - const { context, logger } = createContext(events); + const context = createContext(events); const pathScope = new AcpPathScope({ allowedRoots: [], cwd: root }); const fileSystem = createFileSystem(root, pathScope); @@ -227,7 +213,6 @@ describe("ACP file system bridge", () => { expect(await readFile(join(outside, "write.txt"), "utf8")).toBe("outside"); expect(events[0]).toMatchObject({ payload: { path: join(retained, "write.txt") } }); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(retained, { force: true, recursive: true }); await rm(outside, { force: true, recursive: true }); @@ -299,7 +284,7 @@ describe("ACP file system bridge", () => { const outsideLeaf = join(outside, "leaf.txt"); const outsideAncestor = join(outside, "note.txt"); const events: DriverEventInput[] = []; - const { context, logger } = createContext(events); + const context = createContext(events); class ExchangingPathScope extends AcpPathScope { override async openWritable(path: string, label: string) { @@ -329,7 +314,6 @@ describe("ACP file system bridge", () => { expect(await readFile(outsideAncestor, "utf8")).toBe("outside ancestor"); expect(events[0]).toMatchObject({ payload: { path: join(retained, "note.txt") } }); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(outside, { force: true, recursive: true }); } @@ -339,7 +323,7 @@ describe("ACP file system bridge", () => { const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-mode-")); const path = join(root, "script.sh"); const events: DriverEventInput[] = []; - const { context, logger } = createContext(events); + const context = createContext(events); try { await writeFile(path, "old"); @@ -349,7 +333,6 @@ describe("ACP file system bridge", () => { expect(await readFile(path, "utf8")).toBe("new"); expect((await stat(path)).mode & 0o777).toBe(0o751); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -358,7 +341,7 @@ describe("ACP file system bridge", () => { const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-abort-")); const path = join(root, "note.txt"); const events: DriverEventInput[] = []; - const { context, logger } = createContext(events); + const context = createContext(events); const aborted = new DOMException("test abort", "AbortError"); let checkpoints = 0; const signal = { @@ -383,7 +366,6 @@ describe("ACP file system bridge", () => { expect((await readdir(root)).filter((entry) => entry.endsWith(".tmp"))).toEqual([]); expect(events).toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); diff --git a/tests/acp-terminal-manager.test.ts b/tests/acp-terminal-manager.test.ts index 4d372d1..8725605 100644 --- a/tests/acp-terminal-manager.test.ts +++ b/tests/acp-terminal-manager.test.ts @@ -5,7 +5,7 @@ import { mkdir, mkdtemp, readFile, rename, rm, symlink } from "node:fs/promises" import { tmpdir } from "node:os"; import { join } from "node:path"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { createDriverId, isDriverId } from "../src/protocol/id"; import { spawnLinuxProcessTreeWatchdog } from "../src/runtimes/child-process"; @@ -24,17 +24,12 @@ function createHarness( cwd = process.cwd(), ) { const events: DriverEventInput[] = []; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "acp-terminal-manager-test", - sink: async () => {}, - }); const context = createAgentDriverContext({ eventSink: { currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload: driverStartInput, permission: { request: async () => "reject_once" }, }); @@ -56,7 +51,7 @@ function createHarness( ...(spawnWatchdog === undefined ? {} : { spawnWatchdog }), }); - return { context, events, logger, manager }; + return { context, events, manager }; } function isProcessRunning(pid: number): boolean { @@ -133,7 +128,6 @@ describe("ACP terminal manager", () => { }); } finally { await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(outside, { force: true, recursive: true }); } @@ -161,7 +155,6 @@ describe("ACP terminal manager", () => { await harness.manager.release(harness.context, { terminalId }); } finally { await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); @@ -198,7 +191,6 @@ describe("ACP terminal manager", () => { } finally { cleanup.resolve(); await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); @@ -233,7 +225,6 @@ describe("ACP terminal manager", () => { } finally { cleanup.resolve(); await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); @@ -281,7 +272,6 @@ describe("ACP terminal manager", () => { } finally { allowExit.resolve(); await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); @@ -322,7 +312,6 @@ describe("ACP terminal manager", () => { if (pid > 0 && isProcessRunning(pid)) { process.kill(pid, "SIGKILL"); } - await harness.logger.destroy(); await rm(directory, { force: true, recursive: true }); } }, @@ -359,7 +348,6 @@ describe("ACP terminal manager", () => { expect(() => harness.manager.output(terminal)).toThrow("does not exist"); } finally { await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }, ); @@ -427,7 +415,6 @@ setInterval(() => {}, 1_000); process.kill(pid, "SIGKILL"); } } - await harness.logger.destroy(); await rm(directory, { force: true, recursive: true }); } }, 7_000); @@ -473,7 +460,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 process.kill(pid, "SIGKILL"); } } - await harness.logger.destroy(); await rm(directory, { force: true, recursive: true }); } }); @@ -501,23 +487,18 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 expect(supervisors.size).toBe(1); } finally { await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); test("rejects terminal buffers above the deployment hard limit", async () => { const harness = createHarness(); - try { - await expect( - harness.manager.create(harness.context, { - command: process.execPath, - outputByteLimit: 1_024 * 1_024 + 1, - }), - ).rejects.toThrow("exceeds 1048576 bytes"); - } finally { - await harness.logger.destroy(); - } + await expect( + harness.manager.create(harness.context, { + command: process.execPath, + outputByteLimit: 1_024 * 1_024 + 1, + }), + ).rejects.toThrow("exceeds 1048576 bytes"); }); test("bounds retained terminals and restores capacity after release", async () => { @@ -541,7 +522,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 await harness.manager.release(harness.context, second); } finally { await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); @@ -574,7 +554,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 await harness.manager.release(harness.context, next); } finally { await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); @@ -604,7 +583,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 expect(() => harness.manager.output(second)).toThrow("does not exist"); } finally { await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); @@ -629,7 +607,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 await harness.manager.release(harness.context, terminal); } finally { await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); @@ -650,7 +627,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 await harness.manager.release(harness.context, terminal); } finally { await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); @@ -675,7 +651,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 await harness.manager.release(harness.context, terminal); } finally { await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); @@ -692,7 +667,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 ).rejects.toThrow("stopping"); } finally { await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); @@ -724,7 +698,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } finally { release.resolve(); await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); @@ -769,7 +742,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } finally { release.resolve(); await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); @@ -823,7 +795,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } finally { release.resolve(); await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); @@ -881,7 +852,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 failRelease = false; releaseCreated.resolve(); await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); @@ -911,7 +881,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } } finally { await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); @@ -932,7 +901,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 await harness.manager.release(harness.context, terminal); } finally { await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); @@ -976,7 +944,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 expect(() => harness.manager.output(terminal)).toThrow("does not exist"); } finally { await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }, ); @@ -1020,7 +987,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 }); } finally { await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }); @@ -1058,7 +1024,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } finally { cleanup.resolve(); await harness.manager.stopAll(harness.context).catch(() => {}); - await harness.logger.destroy(); } }, 5_000); @@ -1100,7 +1065,6 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } finally { allowRelease.resolve(); await harness.manager.stopAll(harness.context); - await harness.logger.destroy(); } }); }); diff --git a/tests/agent-backend-lifecycle.test.ts b/tests/agent-backend-lifecycle.test.ts index 1b54099..97ecb87 100644 --- a/tests/agent-backend-lifecycle.test.ts +++ b/tests/agent-backend-lifecycle.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, jest, test } from "bun:test"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { AgentBackendLifecycle } from "../src/core/agent-backend-lifecycle"; @@ -59,8 +59,6 @@ describe("AgentBackendLifecycle", () => { }); test("bounds a final stop and serializes its retry behind late cleanup", async () => { - type ManualTimer = { active: boolean; delay: number; run: () => void }; - const backend = createBackend(); const startEntered = Promise.withResolvers(); const releaseStart = Promise.withResolvers(); @@ -107,52 +105,18 @@ describe("AgentBackendLifecycle", () => { startTimeoutMs: 10_000, stopTimeoutMs: 1_000, }); - const start = lifecycle.start(); - await startEntered.promise; - - const nativeClearTimeout = globalThis.clearTimeout; - const nativeNow = Date.now; - const nativeSetTimeout = globalThis.setTimeout; - const timers = new Set(); - let now = 0; - Date.now = () => now; - globalThis.setTimeout = (( - callback: (...args: unknown[]) => void, - delay = 0, - ...args: unknown[] - ) => { - const timer: ManualTimer = { - active: true, - delay, - run: () => { - if (timer.active) { - callback(...args); - } - }, - }; - timers.add(timer); - return timer as unknown as ReturnType; - }) as typeof setTimeout; - globalThis.clearTimeout = ((handle: ReturnType) => { - const timer = handle as unknown as ManualTimer; - if (timers.has(timer)) { - timer.active = false; - } else { - nativeClearTimeout(handle); - } - }) as typeof clearTimeout; + jest.useFakeTimers({ now: 0 }); try { + const start = lifecycle.start(); + await startEntered.promise; const shutdown = lifecycle.shutdown("test shutdown"); await firstStopEntered.promise; - now = 400; + jest.setSystemTime(400); releaseStart.resolve(); await start; await finalStopEntered.promise; - - const [finalTimer] = [...timers].filter((timer) => timer.active); - expect(finalTimer?.delay).toBe(600); - finalTimer?.run(); + jest.advanceTimersByTime(600); await finalStopAborted.promise; await expect(shutdown).rejects.toThrow("test final stop timed out after 600ms"); @@ -167,9 +131,7 @@ describe("AgentBackendLifecycle", () => { } finally { releaseStart.resolve(); releaseFinalStop.resolve(); - Date.now = nativeNow; - globalThis.clearTimeout = nativeClearTimeout; - globalThis.setTimeout = nativeSetTimeout; + jest.useRealTimers(); } }); }); diff --git a/tests/architecture-boundaries.test.ts b/tests/architecture-boundaries.test.ts index f3b9ab5..b142fa8 100644 --- a/tests/architecture-boundaries.test.ts +++ b/tests/architecture-boundaries.test.ts @@ -459,41 +459,6 @@ function compositionViolations(graph: SourceGraph): string[] { .toSorted(); } -function fileCycles(graph: SourceGraph): string[] { - const visited = new Set(); - const visiting = new Set(); - const stack: string[] = []; - const cycles = new Set(); - - function visit(file: string): void { - if (visited.has(file)) { - return; - } - - visiting.add(file); - stack.push(file); - - for (const dependency of graph.dependencies.get(file) ?? []) { - if (visiting.has(dependency)) { - const start = stack.indexOf(dependency); - cycles.add([...stack.slice(start), dependency].map(sourcePath).join(" -> ")); - } else { - visit(dependency); - } - } - - stack.pop(); - visiting.delete(file); - visited.add(file); - } - - for (const file of graph.files) { - visit(file); - } - - return [...cycles].toSorted(); -} - function sourceDirectory(path: string): string { const parts = relative(sourceRoot, path).split(sep); return parts.length === 1 ? "(entrypoints)" : parts[0]!; @@ -610,8 +575,10 @@ describe("source architecture", () => { ]); }); - test("has no file-level import cycles", () => { - expect(fileCycles(graph)).toEqual([]); + test("has no file-level dependency cycles", () => { + expect( + graph.files.filter((file) => reachableDependencies(graph, file).has(file)).map(sourcePath), + ).toEqual([]); }); test("has no bidirectional top-level source directory dependencies", () => { diff --git a/tests/claude-agent-sdk-driver-backend.test.ts b/tests/claude-agent-sdk-driver-backend.test.ts index 0a7edf5..0261256 100644 --- a/tests/claude-agent-sdk-driver-backend.test.ts +++ b/tests/claude-agent-sdk-driver-backend.test.ts @@ -8,7 +8,7 @@ import type { } from "@anthropic-ai/claude-agent-sdk"; import { DriverTurnCancelledError } from "../src/core/driver-runtime-state"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; import type { DriverStartInput } from "../src/protocol/start"; @@ -119,11 +119,6 @@ function createHarness( const events: DriverEventInput[] = []; let currentRunId: RunId | null = null; let seq = 0; - const logger = createBufferedSinkLogger({ - level: "error", - service: "claude-agent-sdk-driver-backend-test", - sink: async () => {}, - }); const payload = payloadOverride ?? ({ @@ -154,7 +149,7 @@ function createHarness( }; }, }, - logger, + logger: createDisabledLogger(), payload, permission: { request: async () => "allow_once" }, ports: { skill: { materialize: async () => [] } }, @@ -164,7 +159,6 @@ function createHarness( backend: new ClaudeAgentSdkDriverBackend(payload, dependencies), context, events, - logger, }; } @@ -204,7 +198,6 @@ describe("Claude Agent SDK driver backend", () => { expect(harness.events.some((event) => event.kind === "runtime.resume.updated")).toBe(false); expect(harness.events.some((event) => event.kind === "run.failed")).toBe(true); expect(JSON.stringify(harness.events)).not.toContain(oversizedSessionId); - await harness.logger.destroy(); const emptyHarness = createHarness({ createQueryOptions: async () => ({}), @@ -223,7 +216,6 @@ describe("Claude Agent SDK driver backend", () => { expect(emptyHarness.events.some((event) => event.kind === "runtime.resume.updated")).toBe( false, ); - await emptyHarness.logger.destroy(); }); test("consumes a ready prewarm for the first turn", async () => { @@ -258,7 +250,6 @@ describe("Claude Agent SDK driver backend", () => { await nextEventLoopTurn(); await harness.backend.handleInput(harness.context, { text: "first" }, DRIVER_TEST_IDS.runId); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(prompts).toEqual(["first"]); expect(coldQueries).toBe(0); @@ -310,7 +301,6 @@ describe("Claude Agent SDK driver backend", () => { DRIVER_TEST_IDS.secondRunId, ); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(coldQueries).toBe(2); expect(warmQueries).toBe(0); @@ -360,7 +350,6 @@ describe("Claude Agent SDK driver backend", () => { harness.backend.stop(harness.context, "test.stop", new AbortController().signal), ).resolves.toBeUndefined(); expect(cleanupRetries).toBe(1); - await harness.logger.destroy(); }); test("stop joins a prewarm that ignores cancellation until startup settles", async () => { @@ -407,7 +396,6 @@ describe("Claude Agent SDK driver backend", () => { expect(stopped).toBe(false); processExit.resolve(); await stopping; - await harness.logger.destroy(); }); test("stop joins a cooperatively aborted prewarm", async () => { @@ -436,7 +424,6 @@ describe("Claude Agent SDK driver backend", () => { harness.backend.stop(harness.context, "test.stop", new AbortController().signal), ).resolves.toBeUndefined(); expect(warmSignal?.aborted).toBe(true); - await harness.logger.destroy(); }); test("stop fails at its deadline and can retry when prewarm later settles", async () => { @@ -476,7 +463,6 @@ describe("Claude Agent SDK driver backend", () => { }); await expect(retry).resolves.toBeUndefined(); expect(closes).toBe(1); - await harness.logger.destroy(); }); test("stop consumes a late prewarm rejection", async () => { @@ -508,7 +494,6 @@ describe("Claude Agent SDK driver backend", () => { startup.reject(new Error("late startup failure")); await expect(stopping).resolves.toBeUndefined(); - await harness.logger.destroy(); }); test("stop propagates a rejected prewarm process cleanup", async () => { @@ -552,7 +537,6 @@ describe("Claude Agent SDK driver backend", () => { harness.backend.stop(harness.context, "test.stop.retry", new AbortController().signal), ).resolves.toBeUndefined(); expect(cleanupRetries).toBe(1); - await harness.logger.destroy(); }); test("retains a spontaneous prewarm process cleanup rejection", async () => { @@ -581,7 +565,6 @@ describe("Claude Agent SDK driver backend", () => { await expect( harness.backend.stop(harness.context, "test.stop", new AbortController().signal), ).rejects.toBe(cleanupError); - await harness.logger.destroy(); }); test("does not lose a permanent prewarm cleanup failure while retrying its sibling", async () => { @@ -622,7 +605,6 @@ describe("Claude Agent SDK driver backend", () => { harness.backend.stop(harness.context, "test.stop.retry", new AbortController().signal), ).rejects.toBe(permanentError); expect(cleanupRetries).toBe(1); - await harness.logger.destroy(); }); test("concurrent stops share the same prewarm join", async () => { @@ -668,7 +650,6 @@ describe("Claude Agent SDK driver backend", () => { await Promise.all([first, second]); expect(closes).toBe(1); - await harness.logger.destroy(); }); test("stop drains prewarm cleanup after active turn cleanup fails", async () => { @@ -752,7 +733,6 @@ describe("Claude Agent SDK driver backend", () => { { reason: activeCleanupError, status: "rejected" }, { reason: activeCleanupError, status: "rejected" }, ]); - await harness.logger.destroy(); expect( harness.events.some((event) => @@ -806,7 +786,6 @@ describe("Claude Agent SDK driver backend", () => { { kind: "run.cancelled", runId: DRIVER_TEST_IDS.runId }, ]); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); }, ); @@ -826,7 +805,6 @@ describe("Claude Agent SDK driver backend", () => { harness.backend.handleInput(harness.context, { text: "hello" }, DRIVER_TEST_IDS.runId), ).rejects.toThrow("query options failed"); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect( harness.events.filter((event) => @@ -883,7 +861,6 @@ describe("Claude Agent SDK driver backend", () => { processExit.resolve(); await expect(handling).rejects.toThrow("query creation failed"); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(processTasks?.size).toBe(0); expect(harness.events.some((event) => event.kind === "run.failed")).toBe(true); @@ -916,7 +893,6 @@ describe("Claude Agent SDK driver backend", () => { harness.backend.handleInput(harness.context, { text: "hello" }, DRIVER_TEST_IDS.runId), ).rejects.toThrow("event sink unavailable"); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(queryCalls).toBe(0); expect( @@ -988,7 +964,6 @@ describe("Claude Agent SDK driver backend", () => { releaseLateProcess.resolve(); await handling; await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(harness.events.some((event) => event.kind === "run.completed")).toBe(true); }); @@ -1052,7 +1027,6 @@ describe("Claude Agent SDK driver backend", () => { await expect( harness.backend.stop(harness.context, "test.stop.retry", new AbortController().signal), ).resolves.toBeUndefined(); - await harness.logger.destroy(); expect(cleanupRetries).toBe(1); expect( @@ -1097,7 +1071,6 @@ describe("Claude Agent SDK driver backend", () => { await expect( harness.backend.stop(harness.context, "test.complete", new AbortController().signal), ).resolves.toBeUndefined(); - await harness.logger.destroy(); expect( harness.events.some((event) => @@ -1195,7 +1168,6 @@ describe("Claude Agent SDK driver backend", () => { await cancellation; await expect(handling).rejects.toBeInstanceOf(DriverTurnCancelledError); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(closes).toBe(1); expect( @@ -1289,7 +1261,6 @@ describe("Claude Agent SDK driver backend", () => { { reason: cleanupError, status: "rejected" }, { reason: cleanupError, status: "rejected" }, ]); - await harness.logger.destroy(); expect( harness.events.some((event) => @@ -1329,7 +1300,6 @@ describe("Claude Agent SDK driver backend", () => { releaseFinish.resolve(); await expect(handling).resolves.toBeUndefined(); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(closes).toBe(1); expect( @@ -1374,7 +1344,6 @@ describe("Claude Agent SDK driver backend", () => { harness.backend.handleInput(harness.context, { text: "finish" }, DRIVER_TEST_IDS.runId), ).rejects.toThrow("terminal delivery unavailable"); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(terminalAttempts).toEqual([[terminal], [terminal]]); }, @@ -1411,7 +1380,6 @@ describe("Claude Agent SDK driver backend", () => { await stopping; await optionsReturned.promise; await Promise.resolve(); - await harness.logger.destroy(); expect(startupCalls).toBe(0); }); @@ -1432,7 +1400,6 @@ describe("Claude Agent SDK driver backend", () => { harness.backend.handleInput(harness.context, { text: "second" }, DRIVER_TEST_IDS.secondRunId), ).rejects.toThrow("different native session"); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect( harness.events.filter((event) => @@ -1486,7 +1453,6 @@ describe("Claude Agent SDK driver backend", () => { DRIVER_TEST_IDS.secondRunId, ); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(optionSessionIds).toEqual([null, "native-session-1"]); expect( @@ -1548,7 +1514,6 @@ describe("Claude Agent SDK driver backend", () => { DRIVER_TEST_IDS.secondRunId, ); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(lateFrameRead).toBe(false); expect( @@ -1618,7 +1583,6 @@ describe("Claude Agent SDK driver backend", () => { DRIVER_TEST_IDS.secondRunId, ); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(prompts).toHaveLength(2); expect(prompts[0]).toContain(""); @@ -1658,7 +1622,6 @@ describe("Claude Agent SDK driver backend", () => { DRIVER_TEST_IDS.runId, ); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - await harness.logger.destroy(); expect(prompts).toEqual(["add a page"]); }); diff --git a/tests/claude-agent-sdk-durability.test.ts b/tests/claude-agent-sdk-durability.test.ts index 1cb4e6b..56e874b 100644 --- a/tests/claude-agent-sdk-durability.test.ts +++ b/tests/claude-agent-sdk-durability.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from "bun:test"; import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-event-envelope"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { DriverBootPayload } from "../src/protocol/boot"; import type { DriverEventInput } from "../src/protocol/events"; import { createDriverId } from "../src/protocol/id"; @@ -51,17 +51,12 @@ function createCmaHarness() { const sessionId = createDriverId() as SessionId; const events: DriverEventInput[] = []; const sseFrameBytes: number[] = []; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "claude-agent-sdk-durability-test", - sink: async () => {}, - }); const context = createAgentDriverContext({ eventSink: { currentRunId: () => runId, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload: bootPayload, permission: { request: async () => "allow_once" }, }); @@ -91,7 +86,7 @@ function createCmaHarness() { replaceNativeSessionId: async () => {}, }); - return { context, events, logger, runId, sseFrameBytes, translator }; + return { context, events, runId, sseFrameBytes, translator }; } describe("Claude Agent SDK durable event boundaries", () => { @@ -117,7 +112,6 @@ describe("Claude Agent SDK durable event boundaries", () => { expect(messageText(harness.events, finalMessageId as string)).toBe(text); expect(payload(terminal!)).not.toHaveProperty("finalMessageText"); expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await harness.logger.destroy(); }); test("rejects oversized structured output before choosing completed closures", async () => { @@ -146,7 +140,6 @@ describe("Claude Agent SDK durable event boundaries", () => { expect(harness.events.map(({ kind }) => kind)).not.toContain("run.completed"); expect(harness.events.map(({ kind }) => kind)).not.toContain("message.completed"); expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await harness.logger.destroy(); }); test("keeps accepted structured output inside the real CMA and SSE boundary", async () => { @@ -160,7 +153,6 @@ describe("Claude Agent SDK durable event boundaries", () => { expect(harness.events.map(({ kind }) => kind)).toContain("run.completed"); expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await harness.logger.destroy(); }); test("bounds oversized provider errors before terminal delivery", async () => { @@ -192,7 +184,6 @@ describe("Claude Agent SDK durable event boundaries", () => { }); expect(JSON.stringify(failed)).not.toContain(providerError); expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await harness.logger.destroy(); }); test("bounds oversized cancellation reasons before terminal delivery", async () => { @@ -209,7 +200,6 @@ describe("Claude Agent SDK durable event boundaries", () => { }); expect(JSON.stringify(cancelled)).not.toContain(reason); expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await harness.logger.destroy(); }); test("stores one copy of a large tool result and fails closed on oversized structured output", async () => { @@ -246,7 +236,6 @@ describe("Claude Agent SDK durable event boundaries", () => { expect(acceptedResult).toBeDefined(); expect(payload(acceptedResult!)).not.toHaveProperty("rawOutput"); expect(accepted.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await accepted.logger.destroy(); const rejected = createCmaHarness(); await rejected.translator.handleSdkMessage( @@ -311,7 +300,6 @@ describe("Claude Agent SDK durable event boundaries", () => { payload(rejected.events.find((event) => event.kind === "run.failed")!)["error"], ).toMatchObject({ code: "claude.tool_result_too_large" }); expect(rejected.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await rejected.logger.destroy(); }); test("rejects an oversized tool input before it can poison terminal delivery", async () => { @@ -324,11 +312,6 @@ describe("Claude Agent SDK durable event boundaries", () => { const events: DriverEventInput[] = []; const sseFrameBytes: number[] = []; const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); - const logger = createBufferedSinkLogger({ - level: "debug", - service: "claude-agent-sdk-published-durability-test", - sink: async () => {}, - }); let activeRunId: RunId | null = runId; let sequence = 0; const context = createAgentDriverContext({ @@ -358,7 +341,7 @@ describe("Claude Agent SDK durable event boundaries", () => { }; }, }, - logger, + logger: createDisabledLogger(), payload: createDriverStartInputFromBootPayload(claudeBootPayload), permission: { request: async () => "allow_once" }, }); @@ -403,7 +386,6 @@ describe("Claude Agent SDK durable event boundaries", () => { expect(events.some((event) => event.kind === "run.failed")).toBe(true); expect(events.some((event) => payload(event)["rawInput"] !== undefined)).toBe(false); expect(sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await logger.destroy(); }); test("closes a retained background task start before terminal failure", async () => { @@ -414,11 +396,6 @@ describe("Claude Agent SDK durable event boundaries", () => { runtimeTransport: "claude-agent-sdk", } satisfies DriverBootPayload; const events: DriverEventInput[] = []; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "claude-agent-sdk-task-retention-test", - sink: async () => {}, - }); let activeRunId: RunId | null = runId; let acceptDelivery = false; let sequence = 0; @@ -441,7 +418,7 @@ describe("Claude Agent SDK durable event boundaries", () => { }; }, }, - logger, + logger: createDisabledLogger(), payload: createDriverStartInputFromBootPayload(claudeBootPayload), permission: { request: async () => "allow_once" }, }); @@ -485,7 +462,6 @@ describe("Claude Agent SDK durable event boundaries", () => { { active: false, taskId: "task-1" }, ]); expect(events.at(-1)?.kind).toBe("run.failed"); - await logger.destroy(); }); test("maps oversized native tool IDs and rejects oversized tool names before durable state", async () => { @@ -551,7 +527,6 @@ describe("Claude Agent SDK durable event boundaries", () => { ).toBe(true); expect(JSON.stringify(accepted.events)).not.toContain(nativeToolCallId); expect(accepted.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await accepted.logger.destroy(); const rejected = createCmaHarness(); const oversizedName = "n".repeat(1_100_000); @@ -585,7 +560,6 @@ describe("Claude Agent SDK durable event boundaries", () => { payload(rejected.events.find((event) => event.kind === "run.failed")!)["error"], ).toMatchObject({ code: "claude.tool_start_too_large" }); expect(JSON.stringify(rejected.events)).not.toContain(oversizedName); - await rejected.logger.destroy(); }); test("rejects oversized file paths before publishing a partial durable batch", async () => { @@ -618,7 +592,6 @@ describe("Claude Agent SDK durable event boundaries", () => { payload(harness.events.find((event) => event.kind === "run.failed")!)["error"], ).toMatchObject({ code: "claude.files_persisted_too_large" }); expect(harness.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await harness.logger.destroy(); }); test("bounds mirror errors and commits thought and retraction state only after delivery", async () => { @@ -642,21 +615,15 @@ describe("Claude Agent SDK durable event boundaries", () => { severity: "error", }); expect(mirror.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); - await mirror.logger.destroy(); const reasons: string[] = []; const replayedEvents: DriverEventInput[] = []; let rejectThought = true; let rejectToolRetraction = true; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "claude-agent-sdk-replay-test", - sink: async () => {}, - }); const runId = createDriverId() as RunId; const context = createAgentDriverContext({ eventSink: { currentRunId: () => runId, pushEvents: async () => ({ accepted: [] }) }, - logger, + logger: createDisabledLogger(), payload: bootPayload, permission: { request: async () => "allow_once" }, }); @@ -743,6 +710,5 @@ describe("Claude Agent SDK durable event boundaries", () => { payload(event)["status"] === "cancelled", ), ).toHaveLength(1); - await logger.destroy(); }); }); diff --git a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts index 92e5360..4ccd961 100644 --- a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts @@ -1,22 +1,17 @@ import { describe, expect, test } from "bun:test"; -import { readFileSync } from "node:fs"; import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; -import { createBufferedSinkLogger } from "../src/observability"; -import type { DriverEventInput } from "../src/protocol/events"; -import { isDriverId } from "../src/protocol/id"; import type { MessageId, RunId } from "../src/protocol/id"; -import type { AgentDriverContext } from "../src/core/agent-driver-backend"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; -import { ClaudeAgentSdkMessageTranslator } from "../src/runtimes/claude/agent-sdk-message-translator"; -import { isRecord, messageText } from "./claude-agent-sdk-test-helpers"; -import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; - -interface EventBatch { - readonly events: DriverEventInput[]; - readonly reason: string; -} +import { + createClaudeAgentSdkHarness as createHarness, + isRecord, + messageText, +} from "./claude-agent-sdk-test-helpers"; +import { + normalizeClaudeProviderEvents as normalizeClaudeEvents, + readProviderFixture, +} from "./provider-fixture-test-helpers"; interface ClaudeProviderFixtureCase { readonly expectedEvents: readonly unknown[]; @@ -33,159 +28,21 @@ const claudeFixtureNames = [ "unknown-message-diagnostic", ] as const; -function readJsonFixture(path: string): unknown { - return JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")); -} - function readClaudeProviderFixtureCase(path: string): ClaudeProviderFixtureCase { - const fixture = readJsonFixture(path); - - if (!isRecord(fixture)) { - throw new Error("Claude provider fixture must be an object."); - } - - const messages = fixture["messages"]; - const expectedEvents = fixture["expectedEvents"]; - const expectedNativeSessionIds = fixture["expectedNativeSessionIds"] ?? []; - const runId = fixture["runId"]; + const fixture = readProviderFixture(path, { + arrays: ["expectedEvents", "messages"], + strings: ["runId"], + }); + const expectedNativeSessionIds = fixture.expectedNativeSessionIds ?? []; if ( - !Array.isArray(messages) || - !Array.isArray(expectedEvents) || !Array.isArray(expectedNativeSessionIds) || - typeof runId !== "string" + !expectedNativeSessionIds.every((entry) => typeof entry === "string") ) { - throw new Error("Claude provider fixture shape is malformed."); - } - - if (!expectedNativeSessionIds.every((entry) => typeof entry === "string")) { - throw new Error("Claude provider fixture expectedNativeSessionIds must be strings."); - } - - return { - expectedEvents, - expectedNativeSessionIds, - messages, - runId: runId as RunId, - }; -} - -function collectDriverIds(value: unknown, aliases: Map): void { - if (typeof value === "string") { - if (isDriverId(value) && !aliases.has(value)) { - aliases.set(value, aliases.size === 0 ? "" : ``); - } - return; - } - - if (Array.isArray(value)) { - for (const entry of value) { - collectDriverIds(entry, aliases); - } - return; - } - - if (!isRecord(value)) { - return; - } - - for (const entry of Object.values(value)) { - collectDriverIds(entry, aliases); - } -} - -function isIsoTimestamp(value: string): boolean { - return value.endsWith("Z") && !Number.isNaN(Date.parse(value)); -} - -function normalizeClaudeValue( - value: unknown, - driverIdAliases: ReadonlyMap, - fieldName?: string, -): unknown { - if (typeof value === "string") { - for (const [driverId, alias] of driverIdAliases) { - if (value === driverId) { - return alias; - } - - if (value.startsWith(`${driverId}:`)) { - return `${alias}${value.slice(driverId.length)}`; - } - } - - if (fieldName !== undefined && fieldName.endsWith("At") && isIsoTimestamp(value)) { - return ""; - } - - return value; - } - - if (Array.isArray(value)) { - return value.map((entry) => normalizeClaudeValue(entry, driverIdAliases)); + throw new TypeError(`Provider fixture ${path} has malformed native session IDs.`); } - if (!isRecord(value)) { - return value; - } - - const entries = Object.entries(value).flatMap(([key, entry]): [string, unknown][] => - entry === undefined ? [] : [[key, normalizeClaudeValue(entry, driverIdAliases, key)]], - ); - - return Object.fromEntries(entries); -} - -function normalizeClaudeEvents(events: readonly DriverEventInput[]): unknown[] { - const driverIdAliases = new Map(); - - for (const event of events) { - collectDriverIds(event, driverIdAliases); - } - - return events.map((event) => normalizeClaudeValue(event, driverIdAliases)); -} - -function createHarness() { - const batches: EventBatch[] = []; - const nativeSessionIds: string[] = []; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "claude-agent-sdk-provider-fixtures-test", - sink: async () => {}, - }); - const context: AgentDriverContext = createAgentDriverContext({ - eventSink: { - currentRunId: () => "run-1" as RunId, - pushEvents: async () => ({ accepted: [] }), - }, - logger, - payload: bootPayload, - permission: { - request: async () => "allow_once", - }, - }); - const translator = new ClaudeAgentSdkMessageTranslator({ - publicToolCallId: (nativeToolCallId) => nativeToolCallId, - push: async (_context, reason, events) => { - batches.push({ events, reason }); - }, - pushTerminal: async (_context, reason, closures, terminal) => { - batches.push({ events: [...closures, terminal], reason }); - }, - recordNativeSessionId: async (_context, sessionId) => { - nativeSessionIds.push(sessionId); - }, - replaceNativeSessionId: async () => {}, - }); - - return { - context, - events: () => batches.flatMap((batch) => batch.events), - logger, - nativeSessionIds, - translator, - }; + return { ...fixture, expectedNativeSessionIds }; } describe("Claude Agent SDK provider fixtures", () => { @@ -228,7 +85,7 @@ describe("Claude Agent SDK provider fixtures", () => { // Real SDK wire shape: every envelope (each stream chunk and the complete // assistant replay) carries a distinct uuid; only the API message id inside // message_start / assistant.message is stable across the whole message. - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -297,7 +154,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const startedEvents = events().filter((event) => event.kind === "message.started"); const completedEvents = events().filter((event) => event.kind === "message.completed"); @@ -326,7 +182,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("scopes interleaved subagent stream chunks away from the main message", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -378,7 +234,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const textByMessageId = new Map(); @@ -399,7 +254,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("does not promote a replayed stream-only message stop to canonical final", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -435,7 +290,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const liveText = events().flatMap((event) => { if (event.kind !== "message.delta" || !isRecord(event.payload)) { @@ -456,7 +310,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("materializes a result-only native resume completion", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { type: "message_stop" }, @@ -476,7 +330,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const translated = events(); const started = translated.filter((event) => event.kind === "message.started"); @@ -501,7 +354,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("keeps structured output as the canonical final payload", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); await translator.handleSdkMessage( context, @@ -519,7 +372,6 @@ describe("Claude Agent SDK provider fixtures", () => { } as unknown as SDKMessage, "run-1" as RunId, ); - await logger.destroy(); expect(events().map(({ kind }) => kind)).not.toContain("message.added"); expect(events()).toContainEqual({ @@ -533,7 +385,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("fails closed for a non-JSON structured output", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); await translator.handleSdkMessage( context, @@ -551,7 +403,6 @@ describe("Claude Agent SDK provider fixtures", () => { } as unknown as SDKMessage, "run-1" as RunId, ); - await logger.destroy(); expect(events()).toContainEqual( expect.objectContaining({ @@ -565,7 +416,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("retracts a superseded refusal before publishing its replacement", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { message: { @@ -604,7 +455,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const snapshots = events().flatMap((event) => { if (event.kind !== "message.added" || !isRecord(event.payload)) { @@ -644,7 +494,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("applies refusal fallback retractions idempotently to messages and tool results", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const fallback = { content: "Retrying with fallback model.", direction: "retry", @@ -726,7 +576,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const retractedMessages = events().filter( (event) => @@ -771,7 +620,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("keeps assistant envelopes distinct when they share an API message id", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { message: { content: [{ text: "first", type: "text" }], id: "shared-native" }, @@ -803,7 +652,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const snapshots = events().flatMap((event) => { if (event.kind !== "message.added" || !isRecord(event.payload)) { @@ -827,7 +675,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("turns provider-aborted result frames into runtime cancellation", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); await translator.handleSdkMessage( context, @@ -845,7 +693,6 @@ describe("Claude Agent SDK provider fixtures", () => { } as unknown as SDKMessage, "run-1" as RunId, ); - await logger.destroy(); expect(events()).toContainEqual( expect.objectContaining({ @@ -857,7 +704,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("does not let a late older assistant completion replace the final message", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -894,7 +741,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); const payload = @@ -907,7 +753,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("fails closed when a newer assistant is still incomplete at result success", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { message: { @@ -937,7 +783,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); const payload = @@ -950,7 +795,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("fails closed when an older full frame arrives after a newer incomplete message", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -980,7 +825,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); const payload = @@ -997,7 +841,7 @@ describe("Claude Agent SDK provider fixtures", () => { // scope comes from the burst anchor: parallel tool blocks of one message // are told apart by content-block index, and a message_stop boundary // separates one message's thought stream from the next. - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const stream = (uuid: string, event: Record) => ({ event, type: "stream_event", uuid }) as unknown as SDKMessage; const messages = [ @@ -1058,7 +902,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const toolArguments = events().flatMap((event) => { if (event.kind !== "tool.call.updated" || !isRecord(event.payload)) { @@ -1099,14 +942,12 @@ describe("Claude Agent SDK provider fixtures", () => { const fixture = readClaudeProviderFixtureCase( `./fixtures/providers/claude-agent-sdk/cases/${name}.json`, ); - const { context, events, logger, nativeSessionIds, translator } = createHarness(); + const { context, events, nativeSessionIds, translator } = createHarness(); for (const message of fixture.messages) { await translator.handleSdkMessage(context, message as SDKMessage, fixture.runId); } - await logger.destroy(); - expect(nativeSessionIds).toEqual(fixture.expectedNativeSessionIds); expect(normalizeClaudeEvents(events())).toEqual(fixture.expectedEvents); }); diff --git a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts index 8391032..4e98f46 100644 --- a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts @@ -2,69 +2,16 @@ import { describe, expect, test } from "bun:test"; import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; -import { createBufferedSinkLogger } from "../src/observability"; -import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; -import type { AgentDriverContext } from "../src/core/agent-driver-backend"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; -import { ClaudeAgentSdkMessageTranslator } from "../src/runtimes/claude/agent-sdk-message-translator"; -import { isRecord, messageText } from "./claude-agent-sdk-test-helpers"; -import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; - -interface EventBatch { - readonly events: DriverEventInput[]; - readonly reason: string; -} - -function createHarness(publicToolCallId: (nativeToolCallId: string) => string = (id) => id) { - const batches: EventBatch[] = []; - const nativeSessionIds: string[] = []; - const nativeSessionResets: Array = []; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "claude-agent-sdk-provider-fixtures-test", - sink: async () => {}, - }); - const context: AgentDriverContext = createAgentDriverContext({ - eventSink: { - currentRunId: () => "run-1" as RunId, - pushEvents: async () => ({ accepted: [] }), - }, - logger, - payload: bootPayload, - permission: { - request: async () => "allow_once", - }, - }); - const translator = new ClaudeAgentSdkMessageTranslator({ - publicToolCallId, - push: async (_context, reason, events) => { - batches.push({ events, reason }); - }, - pushTerminal: async (_context, reason, closures, terminal) => { - batches.push({ events: [...closures, terminal], reason }); - }, - recordNativeSessionId: async (_context, sessionId) => { - nativeSessionIds.push(sessionId); - }, - replaceNativeSessionId: async (_context, previousSessionId, nextSessionId) => { - nativeSessionResets.push([previousSessionId, nextSessionId]); - }, - }); - - return { - context, - events: () => batches.flatMap((batch) => batch.events), - logger, - nativeSessionIds, - nativeSessionResets, - translator, - }; -} +import { + createClaudeAgentSdkHarness as createHarness, + isRecord, + messageText, +} from "./claude-agent-sdk-test-helpers"; describe("Claude Agent SDK provider fixtures", () => { test("projects visible task activity without leaking task internals", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { description: "Inspect the repository", @@ -141,7 +88,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const taskEvents = events().filter((event) => event.kind === "agent.task.updated"); expect(taskEvents).toHaveLength(2); @@ -164,7 +110,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("closes visible tasks before the run terminal and resets them between turns", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); await translator.handleSdkMessage( context, @@ -233,11 +179,10 @@ describe("Claude Agent SDK provider fixtures", () => { title: "Inspect the repository again", }, }); - await logger.destroy(); }); test("projects informational, local command, mirror failure, and conversation reset frames", async () => { - const { context, events, logger, nativeSessionResets, translator } = createHarness(); + const { context, events, nativeSessionResets, translator } = createHarness(); const messages = [ { content: "A stop hook blocked continuation.", @@ -279,7 +224,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); expect(events()).toContainEqual( expect.objectContaining({ @@ -326,7 +270,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("cancels a truncated assistant frame", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); await translator.handleSdkMessage( context, @@ -338,7 +282,6 @@ describe("Claude Agent SDK provider fixtures", () => { } as unknown as SDKMessage, "run-1" as RunId, ); - await logger.destroy(); const transcript = events(); expect(transcript).toContainEqual( @@ -354,7 +297,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("fails an assistant frame carrying an SDK error", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); await translator.handleSdkMessage( context, @@ -366,7 +309,6 @@ describe("Claude Agent SDK provider fixtures", () => { } as unknown as SDKMessage, "run-1" as RunId, ); - await logger.destroy(); expect(events()).toContainEqual( expect.objectContaining({ @@ -383,7 +325,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("fails a terminating API error carried by a success result", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); await translator.handleSdkMessage( context, @@ -404,7 +346,6 @@ describe("Claude Agent SDK provider fixtures", () => { } as unknown as SDKMessage, "run-1" as RunId, ); - await logger.destroy(); expect(events()).toContainEqual( expect.objectContaining({ @@ -422,7 +363,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("marks an SDK tool error as failed", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { message: { @@ -450,7 +391,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); expect(events()).toContainEqual( expect.objectContaining({ @@ -461,7 +401,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("classifies wrapper-level tool non-execution metadata", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { message: { @@ -499,7 +439,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); expect(events()).toContainEqual( expect.objectContaining({ @@ -518,7 +457,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("materializes authoritative result permission denials without a tool result", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); await translator.handleSdkMessage( context, @@ -551,7 +490,6 @@ describe("Claude Agent SDK provider fixtures", () => { } as unknown as SDKMessage, "run-1" as RunId, ); - await logger.destroy(); expect(events()).toContainEqual( expect.objectContaining({ @@ -572,7 +510,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("lets result permission denials override earlier tool terminals", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { message: { @@ -619,7 +557,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); for (const toolCallId of ["tool-completed", "tool-cancelled"]) { const statuses = events().flatMap((event) => { @@ -644,7 +581,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("rotates assistant identity across a tool boundary and marks the final message", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { message: { @@ -689,7 +626,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const textMessages = events().flatMap((event) => { if (event.kind !== "message.delta" || !isRecord(event.payload)) { @@ -715,7 +651,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("uses the complete assistant message to repair an incomplete stream snapshot", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -758,7 +694,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); const payload = @@ -790,7 +725,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("drops thought and tool updates after their terminal events", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -875,7 +810,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const translated = events(); const thoughtCompletedIndex = translated.findIndex( @@ -901,7 +835,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("ignores streamed text arriving after its assistant message completed", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { message: { @@ -932,7 +866,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const translated = events(); const completed = translated.find((event) => event.kind === "message.completed"); @@ -963,7 +896,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("repairs streamed tool input with a lossless assistant snapshot", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -995,7 +928,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); expect( events().filter( @@ -1015,7 +947,7 @@ describe("Claude Agent SDK provider fixtures", () => { test.each(["success", "error"] as const)( "closes every open item before a %s result terminal", async (outcome) => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -1066,7 +998,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const translated = events(); const terminalIndex = translated.findIndex((event) => @@ -1098,7 +1029,7 @@ describe("Claude Agent SDK provider fixtures", () => { // envelope uuid, and the aggregated assistant envelope (with the native // message id) arrives before message_stop. The reply must stay one // message instead of rendering as "P" / "ong. …" / full-text duplicates. - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -1149,7 +1080,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const translated = events(); const textMessages = translated.flatMap((event) => { @@ -1195,7 +1125,7 @@ describe("Claude Agent SDK provider fixtures", () => { test("anchors uuid-fractured stream fragments to one closed assistant message", async () => { // One scope streams one message at a time; per-envelope uuids must not // fracture a burst whose message_start frame was lost (YEF-884). - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -1228,7 +1158,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const translated = events(); const started = translated @@ -1253,7 +1182,7 @@ describe("Claude Agent SDK provider fixtures", () => { // envelope uuid; the aggregated assistant envelope that follows in the // same scope is that burst's own aggregation and must not mint a // duplicate message (YEF-884). - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -1288,7 +1217,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const textMessages = events().flatMap((event) => { if (event.kind !== "message.delta" || !isRecord(event.payload)) { @@ -1318,7 +1246,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("keeps a confirmed streamed assistant replay on its original message", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const assistant = { message: { content: [{ text: "canonical", type: "text" }], @@ -1364,7 +1292,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const started = events().filter((event) => event.kind === "message.started"); const completed = events().filter((event) => event.kind === "message.completed"); @@ -1385,7 +1312,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("keeps distinct live assistant envelopes that share a native message id", async () => { - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { message: { id: "shared-native" }, type: "message_start" }, @@ -1431,7 +1358,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const snapshots = events().filter((event) => event.kind === "message.added"); const messageIds = snapshots.flatMap((event) => @@ -1454,7 +1380,7 @@ describe("Claude Agent SDK provider fixtures", () => { // A message_start frame proves the streamed message's native id, so an // assistant envelope with a different native id is a genuinely separate // message even when the text repeats. - const { context, events, logger, translator } = createHarness(); + const { context, events, translator } = createHarness(); const messages = [ { event: { @@ -1498,7 +1424,6 @@ describe("Claude Agent SDK provider fixtures", () => { for (const message of messages) { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - await logger.destroy(); const textMessages = events().flatMap((event) => { if (event.kind !== "message.delta" || !isRecord(event.payload)) { diff --git a/tests/claude-agent-sdk-query-options.test.ts b/tests/claude-agent-sdk-query-options.test.ts index 49ab534..ddcdc46 100644 --- a/tests/claude-agent-sdk-query-options.test.ts +++ b/tests/claude-agent-sdk-query-options.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { @@ -22,14 +22,6 @@ async function createRuntimeHome(): Promise { return runtimeHome; } -function createTestLogger() { - return createBufferedSinkLogger({ - level: "debug", - service: "claude-agent-sdk-query-options-test", - sink: async () => {}, - }); -} - afterEach(async () => { await Promise.all( runtimeHomes.map((runtimeHome) => rm(runtimeHome, { force: true, recursive: true })), @@ -176,7 +168,6 @@ describe("Claude Agent SDK query options", () => { runtime: "claude-agent-sdk", runtimeTransport: "claude-agent-sdk", }); - const logger = createTestLogger(); const permission = Promise.withResolvers<"allow_once" | "reject_once">(); let permissionInput: | Parameters["ports"]["permission"]["request"]>[0] @@ -187,7 +178,7 @@ describe("Claude Agent SDK query options", () => { currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload, permission: { request: async (input, signal) => { @@ -275,7 +266,6 @@ describe("Claude Agent SDK query options", () => { toolCallId: "tool-1", }); expect(permissionSignal).toBe(abortController.signal); - await logger.destroy(); }); test("retains an early permission rejection for terminal cleanup", async () => { @@ -298,13 +288,12 @@ describe("Claude Agent SDK query options", () => { runtimeTransport: "claude-agent-sdk", }); const permissionError = new Error("permission delivery failed"); - const logger = createTestLogger(); const context = createAgentDriverContext({ eventSink: { currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload, permission: { request: async () => { @@ -335,6 +324,5 @@ describe("Claude Agent SDK query options", () => { expect(permissionTasks.size).toBe(1); await expect(drainClaudeTasks(permissionTasks)).rejects.toBe(permissionError); expect(permissionTasks.size).toBe(0); - await logger.destroy(); }); }); diff --git a/tests/claude-agent-sdk-test-helpers.ts b/tests/claude-agent-sdk-test-helpers.ts index 6b68e42..e4701ba 100644 --- a/tests/claude-agent-sdk-test-helpers.ts +++ b/tests/claude-agent-sdk-test-helpers.ts @@ -1,8 +1,54 @@ +import type { AgentDriverContext } from "../src/core/agent-driver-backend"; +import { createAgentDriverContext } from "../src/core/agent-driver-backend"; +import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; +import type { RunId } from "../src/protocol/id"; +import { ClaudeAgentSdkMessageTranslator } from "../src/runtimes/claude/agent-sdk-message-translator"; import { isRecord } from "../src/runtimes/claude/agent-sdk-json"; +import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; export { isRecord } from "../src/runtimes/claude/agent-sdk-json"; +export function createClaudeAgentSdkHarness( + publicToolCallId: (nativeToolCallId: string) => string = (id) => id, +) { + const driverEvents: DriverEventInput[] = []; + const nativeSessionIds: string[] = []; + const nativeSessionResets: Array = []; + const context: AgentDriverContext = createAgentDriverContext({ + eventSink: { + currentRunId: () => "run-1" as RunId, + pushEvents: async () => ({ accepted: [] }), + }, + logger: createDisabledLogger(), + payload: bootPayload, + permission: { request: async () => "allow_once" }, + }); + const translator = new ClaudeAgentSdkMessageTranslator({ + publicToolCallId, + push: async (_context, _reason, events) => { + driverEvents.push(...events); + }, + pushTerminal: async (_context, _reason, closures, terminal) => { + driverEvents.push(...closures, terminal); + }, + recordNativeSessionId: async (_context, sessionId) => { + nativeSessionIds.push(sessionId); + }, + replaceNativeSessionId: async (_context, previousSessionId, nextSessionId) => { + nativeSessionResets.push([previousSessionId, nextSessionId]); + }, + }); + + return { + context, + events: () => driverEvents, + nativeSessionIds, + nativeSessionResets, + translator, + }; +} + export function messageText(events: readonly DriverEventInput[], messageId: unknown): string { let text = ""; diff --git a/tests/cma-sdk.test.ts b/tests/cma-sdk.test.ts index 3e1eca6..226ce85 100644 --- a/tests/cma-sdk.test.ts +++ b/tests/cma-sdk.test.ts @@ -9,8 +9,7 @@ import { CMA_DEFAULT_BETA_HEADER_VALUE, createCmaHttpHandler, } from "../src/surfaces/cma-http"; -import type { CmaSdkError } from "../src/surfaces/cma-sdk"; -import { createCmaSdkClient } from "../src/surfaces/cma-sdk"; +import { CmaSdkClient, type CmaSdkError } from "../src/surfaces/cma-sdk"; import { promiseWithTimeout } from "../src/utils/async"; describe("CMA SDK client", () => { @@ -21,7 +20,7 @@ describe("CMA SDK client", () => { dispatchDriverCommand: async () => undefined, store, }); - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async (input, init) => { const request = new Request(input, init); @@ -48,7 +47,7 @@ describe("CMA SDK client", () => { dispatchDriverCommand: async () => undefined, store, }); - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async (input, init) => handler(new Request(input, init)), }); @@ -66,7 +65,7 @@ describe("CMA SDK client", () => { const entered = Promise.withResolvers(); const reason = new Error(`${scope} cancelled`); let fetchSignal: AbortSignal | undefined; - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async (_input, init) => { fetchSignal = init?.signal ?? undefined; @@ -89,7 +88,7 @@ describe("CMA SDK client", () => { test("times out an unresponsive fetch", async () => { let fetchSignal: AbortSignal | undefined; - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async (_input, init) => { fetchSignal = init?.signal ?? undefined; @@ -109,7 +108,7 @@ describe("CMA SDK client", () => { test("bounds and cancels JSON response bodies", async () => { let canceled = false; - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async () => new Response( @@ -144,7 +143,7 @@ describe("CMA SDK client", () => { dispatchDriverCommand: async () => undefined, store, }); - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async (input, init) => handler(new Request(input, init)), }); @@ -192,7 +191,7 @@ describe("CMA SDK client", () => { dispatchDriverCommand: async () => undefined, store, }); - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async (input, init) => handler(new Request(input, init)), }); @@ -266,7 +265,7 @@ describe("CMA SDK client", () => { .map((event) => `data: ${JSON.stringify(event)}${lineEnding}${blankLineEnding}`) .join(""); const bytes = new TextEncoder().encode(frames); - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async () => new Response( @@ -317,7 +316,7 @@ describe("CMA SDK client", () => { const contentBytes = CMA_MAX_EVENT_BYTES + extraBytes - encoder.encode(emptyFrame).byteLength; const frame = `data: ${JSON.stringify(record(`界${"x".repeat(contentBytes - 3)}`))}\n\r\n`; const bytes = encoder.encode(frame); - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async () => new Response( @@ -382,7 +381,7 @@ describe("CMA SDK client", () => { dispatchDriverCommand: async () => undefined, store, }); - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async (input, init) => handler(new Request(input, init)), }); @@ -401,7 +400,7 @@ describe("CMA SDK client", () => { let canceled = false; let sent = false; const bytes = new Uint8Array(CMA_MAX_EVENT_BYTES + 1).fill("x".charCodeAt(0)); - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async () => new Response( @@ -434,7 +433,7 @@ describe("CMA SDK client", () => { const cleanup = Promise.withResolvers(); const readEntered = Promise.withResolvers(); let fetchSignal: AbortSignal | undefined; - const client = createCmaSdkClient({ + const client = new CmaSdkClient({ baseUrl: "https://driver.test", fetch: async (_input, init) => { fetchSignal = init?.signal ?? undefined; diff --git a/tests/driver-artifact-contract.test.ts b/tests/driver-artifact-contract.test.ts index 7b53f17..61c7c88 100644 --- a/tests/driver-artifact-contract.test.ts +++ b/tests/driver-artifact-contract.test.ts @@ -2,13 +2,12 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { AGENT_DRIVER_VERSION } from "../src/core/version"; +import viteConfig from "../vite.config"; -type DriverPackageExportTarget = - | string - | { - readonly default?: string; - readonly types?: string; - }; +interface DriverPackageExportTarget { + readonly default?: string; + readonly types?: string; +} interface DriverPackageJson { readonly bin?: Record; @@ -147,6 +146,32 @@ describe("driver artifact contract", () => { expect(packageJson.exports).not.toHaveProperty("./bin/driver"); }); + test("builds declarations for exactly the public package entries", () => { + const packageJson = readDriverPackageJson(); + const targets = Object.values(packageJson.exports ?? {}); + const sources = [ + ...new Bun.Glob("src/*.ts").scanSync({ + cwd: new URL("../", import.meta.url).pathname, + }), + "src/contract/index.ts", + ].toSorted(); + + expect(packageJson.scripts?.["build:types"]).toBe("vp pack"); + expect(viteConfig.pack).toEqual({ + dts: { emitDtsOnly: true }, + entry: ["src/*.ts", "src/contract/index.ts"], + fixedExtension: false, + outDir: "dist/types", + platform: "neutral", + }); + expect(targets.map(({ default: source }) => source?.slice(2)).toSorted()).toEqual(sources); + for (const { default: source, types } of targets) { + expect(source).toMatch(/^\.\/src\/.+\.ts$/); + expect(types).toBe(source?.replace("./src/", "./dist/types/").replace(/\.ts$/, ".d.ts")); + expect(source).not.toContain("/generated"); + } + }); + test("uses package.json as the runtime version source", () => { const packageJson = readDriverPackageJson(); const runtimeVersionSources = [ @@ -339,7 +364,6 @@ describe("driver artifact contract", () => { const packageJson = readDriverPackageJson(); const deps = Object.keys(packageJson.dependencies ?? {}); const tsconfig = readText("../tsconfig.json"); - const typesTsconfig = readText("../tsconfig.types.json"); expect(deps.filter((dependency) => dependency.startsWith("@mosoo/"))).toEqual([]); expect(packageJson.dependencies).not.toHaveProperty("@cfworker/json-schema"); @@ -347,9 +371,5 @@ describe("driver artifact contract", () => { expect(packageJson.dependencies).toHaveProperty("vestig"); expect(tsconfig).not.toContain("../../dev/"); expect(tsconfig).not.toContain('"extends"'); - expect(typesTsconfig).not.toContain("../../dev/"); - expect(typesTsconfig).toContain('"declaration": true'); - expect(typesTsconfig).toContain('"emitDeclarationOnly": true'); - expect(typesTsconfig).toContain('"outDir": "dist/types"'); }); }); diff --git a/tests/driver-event-publisher-admission.test.ts b/tests/driver-event-publisher-admission.test.ts index ffc336e..a9da097 100644 --- a/tests/driver-event-publisher-admission.test.ts +++ b/tests/driver-event-publisher-admission.test.ts @@ -6,45 +6,13 @@ import { withSourceEventIds, } from "../src/core/driver-runtime-io"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; -import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; import type { RunId } from "../src/protocol/id"; import type { DriverEventBatchOutput } from "../src/protocol/orpc"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; -import { bootPayload } from "./driver-runtime-boundary-fixtures"; - -function createTestLogger() { - return createBufferedSinkLogger({ - level: "debug", - service: "driver-event-publisher-test", - sink: async () => {}, - }); -} - -function createEvent(kind: "message.started" | "message.completed"): DriverEventInput { - return { - kind, - payload: { - messageId: "message-1", - ...(kind === "message.started" ? { role: "agent" } : { stopReason: "end_turn" }), - }, - }; -} - -function createDelta(contentDelta: string): DriverEventInput { - return { - delivery: "best_effort", - kind: "message.delta", - payload: { - contentDelta, - messageId: "message-1", - role: "agent", - }, - }; -} +import { createContext, createDelta, createEvent, kinds } from "./driver-event-publisher-fixture"; function createRunTerminal( kind: "run.cancelled" | "run.completed" | "run.failed", @@ -61,10 +29,6 @@ function createRunTerminal( }; } -function kinds(batches: readonly (readonly DriverEventInput[])[]): string[][] { - return batches.map((batch) => batch.map((event) => event.kind)); -} - function acceptEvents( events: readonly DriverEventInput[], sequence: (index: number) => number = (index) => index + 1, @@ -78,24 +42,6 @@ function acceptEvents( }; } -function createContext(input: { - currentRunId?: () => RunId | null; - pushEvents: (events: DriverEventInput[], signal?: AbortSignal) => Promise; -}) { - return createAgentDriverContext({ - eventSink: { - commandUpdate: async () => {}, - currentRunId: input.currentRunId ?? (() => DRIVER_TEST_IDS.runId), - pushEvents: async ({ events, signal }) => input.pushEvents(events, signal), - }, - logger: createTestLogger(), - payload: bootPayload, - permission: { - request: async () => "reject_once", - }, - }); -} - describe("DriverEventPublisher", () => { test("keeps an explicit source ID across direct lossless retries", async () => { const attempts: DriverEventInput[][] = []; @@ -254,7 +200,6 @@ describe("DriverEventPublisher", () => { expect(isDriverId(attempts[0]?.[0]?.sourceEventId)).toBe(true); expect(isDriverId(attempts[1]?.[1]?.sourceEventId)).toBe(true); expect(publisher.lastAcceptedSeq()).toBe(41); - await context.logger.destroy(); }); test("rejects new events when failed delivery fills the bounded queue", async () => { @@ -270,7 +215,6 @@ describe("DriverEventPublisher", () => { await expect( publisher.push(context, "overflow", [createEvent("message.completed")]), ).rejects.toThrow("Driver event queue exceeds 1024 events."); - await context.logger.destroy(); }); test("does not let a timed-out best-effort lane poison later lossless delivery", async () => { @@ -305,7 +249,6 @@ describe("DriverEventPublisher", () => { ]); } finally { AbortSignal.timeout = nativeTimeout; - await context.logger.destroy(); } expect(signals).toHaveLength(3); @@ -319,39 +262,31 @@ describe("DriverEventPublisher", () => { const releaseFirstSend = Promise.withResolvers(); const attempts: DriverEventInput[][] = []; let activeRunId = DRIVER_TEST_IDS.runId as RunId; - const logger = createTestLogger(); - const context = createAgentDriverContext({ - eventSink: { - currentRunId: () => activeRunId, - pushEvents: async ({ events }) => { - const canonical = events.flatMap((event) => - toDriverEventEnvelopes(driverBootPayload, event, activeRunId), - ); - attempts.push(canonical.map(({ event }) => event)); + const context = createContext({ + currentRunId: () => activeRunId, + pushEvents: async (events) => { + const canonical = events.flatMap((event) => + toDriverEventEnvelopes(driverBootPayload, event, activeRunId), + ); + attempts.push(canonical.map(({ event }) => event)); - if (attempts.length === 1) { - firstSendEntered.resolve(); - await releaseFirstSend.promise; - } + if (attempts.length === 1) { + firstSendEntered.resolve(); + await releaseFirstSend.promise; + } - if (attempts.length === 2) { - activeRunId = DRIVER_TEST_IDS.thirdRunId; - } + if (attempts.length === 2) { + activeRunId = DRIVER_TEST_IDS.thirdRunId; + } - const accepted = attempts.length === 2 ? canonical.slice(0, 1) : canonical; - return { - accepted: accepted.map((envelope, index) => ({ - eventId: envelope.eventId, - seq: attempts.length * 10 + index, - type: envelope.event.kind, - })), - }; - }, - }, - logger, - payload: bootPayload, - permission: { - request: async () => "reject_once", + const accepted = attempts.length === 2 ? canonical.slice(0, 1) : canonical; + return { + accepted: accepted.map((envelope, index) => ({ + eventId: envelope.eventId, + seq: attempts.length * 10 + index, + type: envelope.event.kind, + })), + }; }, }); const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); @@ -371,7 +306,6 @@ describe("DriverEventPublisher", () => { [DRIVER_TEST_IDS.runId], ]); expect(attempts[2]?.[0]?.sourceEventId).toBe(attempts[1]?.[1]?.sourceEventId); - await logger.destroy(); }); test("does not block best-effort producers on transport acknowledgements", async () => { @@ -392,8 +326,6 @@ describe("DriverEventPublisher", () => { await first; releaseFirstSend.resolve(); await publisher.push(context, "flush", [createEvent("message.completed")]); - - await context.logger.destroy(); }); test("delivers terminal closures one at a time before the run terminal", async () => { @@ -419,7 +351,6 @@ describe("DriverEventPublisher", () => { true, ); expect(attempts.at(-1)?.[0]?.kind).toBe("run.completed"); - await context.logger.destroy(); }); test("bounds the whole terminal settlement by one delivery deadline", async () => { @@ -488,7 +419,6 @@ describe("DriverEventPublisher", () => { await expect(terminal).resolves.toBeUndefined(); expect(acceptedKinds).toEqual(["message.completed", "message.completed", "run.completed"]); } finally { - await context.logger.destroy(); } }); @@ -530,7 +460,6 @@ describe("DriverEventPublisher", () => { } finally { releaseDrain.resolve(); await ordinary; - await context.logger.destroy(); } }); @@ -563,7 +492,6 @@ describe("DriverEventPublisher", () => { ["run.completed"], ]); expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); - await context.logger.destroy(); }); test("reuses a retained closure identity when the terminal call is retried", async () => { @@ -601,7 +529,6 @@ describe("DriverEventPublisher", () => { ]); expect(new Set(attempts.slice(0, 3).map(([event]) => event?.sourceEventId)).size).toBe(1); expect(kinds([accepted])).toEqual([["message.completed", "run.completed"]]); - await context.logger.destroy(); }); test("joins one in-flight terminal operation and rejects a different occurrence", async () => { @@ -640,7 +567,6 @@ describe("DriverEventPublisher", () => { await Promise.all([first, joined]); expect(kinds(attempts)).toEqual([["message.completed"], ["run.completed"]]); - await context.logger.destroy(); }); test("continues a partially delivered terminal operation without duplicating closures", async () => { @@ -684,7 +610,6 @@ describe("DriverEventPublisher", () => { ), ).toHaveLength(1); expect(new Set(attempts.slice(1, 4).map(([event]) => event?.sourceEventId)).size).toBe(1); - await context.logger.destroy(); }); test("keeps a failed terminal settlement reserved until the same run retries it", async () => { @@ -738,7 +663,6 @@ describe("DriverEventPublisher", () => { ["run.completed"], ]); expect(attempts.flat().some((event) => event.kind === "agent.task.updated")).toBe(false); - await context.logger.destroy(); }); test("does not let a session push cross a terminal installed at its await boundary", async () => { @@ -766,7 +690,6 @@ describe("DriverEventPublisher", () => { await expect(session).rejects.toThrow("terminal settlement slot is full"); await terminal; expect(kinds(attempts)).toEqual([["run.completed"]]); - await context.logger.destroy(); }); test("adopts the identity of a matching generic pending closure", async () => { @@ -802,7 +725,6 @@ describe("DriverEventPublisher", () => { ["run.completed"], ]); expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); - await context.logger.destroy(); }); test("does not duplicate a matching closure already in flight", async () => { @@ -839,7 +761,6 @@ describe("DriverEventPublisher", () => { await Promise.all([ordinary, terminal]); expect(kinds(attempts)).toEqual([["message.completed"], ["run.completed"]]); - await context.logger.destroy(); }); test("does not resend an accepted in-flight closure while its suffix is blocked", async () => { @@ -905,7 +826,6 @@ describe("DriverEventPublisher", () => { expect( accepted.filter((event) => event.sourceEventId === attempts[0]![0]!.sourceEventId), ).toHaveLength(1); - await context.logger.destroy(); }); test("does not revive a matching in-flight closure rejected by the sink", async () => { @@ -941,7 +861,6 @@ describe("DriverEventPublisher", () => { await expect(ordinary).rejects.toThrow("closure rejected"); await expect(terminal).rejects.toThrow("closure rejected"); expect(kinds(attempts)).toEqual([["message.completed"]]); - await context.logger.destroy(); }); test("does not revive a rejected closure while its in-flight suffix is blocked", async () => { @@ -986,7 +905,6 @@ describe("DriverEventPublisher", () => { ["message.completed", "message.started"], ["message.started"], ]); - await context.logger.destroy(); }); test("does not treat a rejected pending terminal retry as accepted", async () => { @@ -1012,7 +930,6 @@ describe("DriverEventPublisher", () => { ).rejects.toThrow("terminal rejected"); expect(kinds(attempts)).toEqual([["run.completed"], ["run.completed"]]); expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); - await context.logger.destroy(); }); test.each([ @@ -1067,8 +984,6 @@ describe("DriverEventPublisher", () => { await expect(terminal).resolves.toBeUndefined(); expect(attempts.at(-1)?.[0]?.kind).toBe("run.completed"); } - - await context.logger.destroy(); }, ); @@ -1111,7 +1026,6 @@ describe("DriverEventPublisher", () => { await retryEntered.promise; } finally { releaseRetry.resolve(); - await context.logger.destroy(); } expect(attempts.flat().some((event) => event.kind === "run.completed")).toBe(false); @@ -1164,7 +1078,6 @@ describe("DriverEventPublisher", () => { await expect(pending).rejects.toThrow("initial transport failure"); await expect(terminal).rejects.toThrow("unsupported"); expect(attempts.flat().some((event) => event.kind === "run.completed")).toBe(false); - await context.logger.destroy(); }); test("rejects aggregate terminal bytes before delivering any event", async () => { @@ -1189,7 +1102,6 @@ describe("DriverEventPublisher", () => { publisher.pushTerminal(context, "terminal", closures, createRunTerminal("run.completed")), ).toThrow("run terminal batch exceeds 1048576 UTF-8 bytes"); expect(sends).toBe(0); - await context.logger.destroy(); }); test("rejects aggregate terminal count before reading payloads", async () => { @@ -1220,7 +1132,6 @@ describe("DriverEventPublisher", () => { ).toThrow("exceeds 1024 events"); expect(payloadReads).toBe(0); expect(sends).toBe(0); - await context.logger.destroy(); }); test("rejects an oversized run terminal before delivering closures", async () => { @@ -1241,7 +1152,6 @@ describe("DriverEventPublisher", () => { publisher.pushTerminal(context, "terminal", [createEvent("message.completed")], terminal), ).toThrow("run terminal batch exceeds 1048576 UTF-8 bytes"); expect(sends).toBe(0); - await context.logger.destroy(); }); test("rejects a non-run terminal before delivering closures", async () => { @@ -1263,7 +1173,6 @@ describe("DriverEventPublisher", () => { ), ).toThrow("requires a run terminal event"); expect(sends).toBe(0); - await context.logger.destroy(); }); test.each([ @@ -1294,7 +1203,6 @@ describe("DriverEventPublisher", () => { "requires unique source event IDs", ); expect(sends).toBe(0); - await context.logger.destroy(); }); test("rejects duplicate source IDs in an ordinary push before delivery", async () => { @@ -1314,7 +1222,6 @@ describe("DriverEventPublisher", () => { ]), ).rejects.toThrow("requires unique source event IDs"); expect(sends).toBe(0); - await context.logger.destroy(); }); test("does not confuse a pending event with a different terminal closure sharing its ID", async () => { @@ -1347,7 +1254,6 @@ describe("DriverEventPublisher", () => { expect(attempts.flat().some((event) => event.kind === "message.completed")).toBe(false); expect(attempts.flat().some((event) => event.kind === "run.completed")).toBe(false); - await context.logger.destroy(); }); test.each([ @@ -1370,7 +1276,6 @@ describe("DriverEventPublisher", () => { expect(() => publisher.pushTerminal(context, "terminal", closures, terminal)).toThrow(); expect(sends).toBe(0); - await context.logger.destroy(); }); test("rejects or drops events queued after a run terminal", async () => { @@ -1394,7 +1299,6 @@ describe("DriverEventPublisher", () => { await expect(lossless).rejects.toThrow("run terminal settlement slot is full"); await expect(Promise.all([terminal, bestEffort])).resolves.toEqual([undefined, undefined]); expect(kinds(attempts)).toEqual([["run.completed"]]); - await context.logger.destroy(); }); test("requires the unique terminal entry point and an attributable run", async () => { @@ -1416,7 +1320,6 @@ describe("DriverEventPublisher", () => { "requires an active run", ); expect(sends).toBe(0); - await context.logger.destroy(); }); test("settles sequential runs after the active run changes", async () => { @@ -1446,7 +1349,6 @@ describe("DriverEventPublisher", () => { ["message.started", null], ["run.completed", DRIVER_TEST_IDS.secondRunId], ]); - await context.logger.destroy(); }); test("keeps session events unscoped without reopening a settled run", async () => { @@ -1488,7 +1390,6 @@ describe("DriverEventPublisher", () => { ["agent.task.updated", null], ["agent.task.updated", null], ]); - await context.logger.destroy(); }); test("does not let another run displace an in-flight settlement", async () => { @@ -1522,7 +1423,6 @@ describe("DriverEventPublisher", () => { await first; expect(attempts).toHaveLength(1); - await context.logger.destroy(); }); test("rejects an operation that targets a run other than the active run", async () => { @@ -1543,6 +1443,5 @@ describe("DriverEventPublisher", () => { }), ).toThrow("must target the active run"); expect(sends).toBe(0); - await context.logger.destroy(); }); }); diff --git a/tests/driver-event-publisher-backpressure.test.ts b/tests/driver-event-publisher-backpressure.test.ts index be2931f..b1dfcf2 100644 --- a/tests/driver-event-publisher-backpressure.test.ts +++ b/tests/driver-event-publisher-backpressure.test.ts @@ -1,65 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; -import type { RunId } from "../src/protocol/id"; -import type { DriverEventBatchOutput } from "../src/protocol/orpc"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; -import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; -import { bootPayload } from "./driver-runtime-boundary-fixtures"; - -function createTestLogger() { - return createBufferedSinkLogger({ - level: "debug", - service: "driver-event-publisher-test", - sink: async () => {}, - }); -} - -function createEvent(kind: "message.started" | "message.completed"): DriverEventInput { - return { - kind, - payload: { - messageId: "message-1", - ...(kind === "message.started" ? { role: "agent" } : { stopReason: "end_turn" }), - }, - }; -} - -function createDelta(contentDelta: string): DriverEventInput { - return { - delivery: "best_effort", - kind: "message.delta", - payload: { - contentDelta, - messageId: "message-1", - role: "agent", - }, - }; -} - -function kinds(batches: readonly (readonly DriverEventInput[])[]): string[][] { - return batches.map((batch) => batch.map((event) => event.kind)); -} - -function createContext(input: { - currentRunId?: () => RunId | null; - pushEvents: (events: DriverEventInput[], signal?: AbortSignal) => Promise; -}) { - return createAgentDriverContext({ - eventSink: { - commandUpdate: async () => {}, - currentRunId: input.currentRunId ?? (() => DRIVER_TEST_IDS.runId), - pushEvents: async ({ events, signal }) => input.pushEvents(events, signal), - }, - logger: createTestLogger(), - payload: bootPayload, - permission: { - request: async () => "reject_once", - }, - }); -} +import { createContext, createDelta, createEvent, kinds } from "./driver-event-publisher-fixture"; describe("DriverEventPublisher", () => { test("resolves only fully accepted callers after a coalesced partial failure", async () => { @@ -92,7 +35,6 @@ describe("DriverEventPublisher", () => { ["message.started", "message.completed"], ["message.completed"], ]); - await context.logger.destroy(); }); test.each([ @@ -149,7 +91,6 @@ describe("DriverEventPublisher", () => { expect(attempts[1]?.map((event) => event.sourceEventId)).toEqual( attempts[0]?.map((event) => event.sourceEventId), ); - await context.logger.destroy(); }, ); }); diff --git a/tests/driver-event-publisher-drain.test.ts b/tests/driver-event-publisher-drain.test.ts index 6d90ea1..abc168f 100644 --- a/tests/driver-event-publisher-drain.test.ts +++ b/tests/driver-event-publisher-drain.test.ts @@ -1,67 +1,11 @@ import { describe, expect, test } from "bun:test"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; -import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; -import type { RunId } from "../src/protocol/id"; -import type { DriverEventBatchOutput } from "../src/protocol/orpc"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; -import { bootPayload } from "./driver-runtime-boundary-fixtures"; - -function createTestLogger() { - return createBufferedSinkLogger({ - level: "debug", - service: "driver-event-publisher-test", - sink: async () => {}, - }); -} - -function createEvent(kind: "message.started" | "message.completed"): DriverEventInput { - return { - kind, - payload: { - messageId: "message-1", - ...(kind === "message.started" ? { role: "agent" } : { stopReason: "end_turn" }), - }, - }; -} - -function createDelta(contentDelta: string): DriverEventInput { - return { - delivery: "best_effort", - kind: "message.delta", - payload: { - contentDelta, - messageId: "message-1", - role: "agent", - }, - }; -} - -function kinds(batches: readonly (readonly DriverEventInput[])[]): string[][] { - return batches.map((batch) => batch.map((event) => event.kind)); -} - -function createContext(input: { - currentRunId?: () => RunId | null; - pushEvents: (events: DriverEventInput[], signal?: AbortSignal) => Promise; -}) { - return createAgentDriverContext({ - eventSink: { - commandUpdate: async () => {}, - currentRunId: input.currentRunId ?? (() => DRIVER_TEST_IDS.runId), - pushEvents: async ({ events, signal }) => input.pushEvents(events, signal), - }, - logger: createTestLogger(), - payload: bootPayload, - permission: { - request: async () => "reject_once", - }, - }); -} +import { createContext, createDelta, createEvent, kinds } from "./driver-event-publisher-fixture"; describe("DriverEventPublisher", () => { test("drains a partial receipt before resolving the same reliable push", async () => { @@ -106,7 +50,6 @@ describe("DriverEventPublisher", () => { ]); expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[1]?.sourceEventId); expect(publisher.lastAcceptedSeq()).toBe(42); - await context.logger.destroy(); }); test("fails a reliable push that makes no receipt progress and retries it later", async () => { @@ -141,7 +84,6 @@ describe("DriverEventPublisher", () => { expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); expect(attempts[1]?.[1]?.sourceEventId).toBe(attempts[0]?.[1]?.sourceEventId); expect(publisher.lastAcceptedSeq()).toBe(52); - await context.logger.destroy(); }); test("retains only the unaccepted suffix after progress stops", async () => { @@ -186,7 +128,6 @@ describe("DriverEventPublisher", () => { ]); expect(attempts[2]?.[0]?.sourceEventId).toBe(attempts[0]?.[1]?.sourceEventId); expect(publisher.lastAcceptedSeq()).toBe(51); - await context.logger.destroy(); }); test("reserves a full lossless lane beside queued best-effort deltas", async () => { @@ -237,7 +178,6 @@ describe("DriverEventPublisher", () => { "message.started", "message.completed", ]); - await context.logger.destroy(); }); test("takes ownership of a queued lossless array", async () => { @@ -269,7 +209,6 @@ describe("DriverEventPublisher", () => { await Promise.all([first, second]); expect(attempts.map((batch) => batch.length)).toEqual([1, 1]); - await context.logger.destroy(); }); test("takes deep ownership of queued lossless payloads", async () => { @@ -324,7 +263,6 @@ describe("DriverEventPublisher", () => { ["original", "message-1"], ]); expect(observed[2]?.sourceEventIds[0]).toBe(observed[1]?.sourceEventIds[0]); - await context.logger.destroy(); }); test("drops a rejected lossless draft without blocking a terminal event", async () => { @@ -356,7 +294,6 @@ describe("DriverEventPublisher", () => { "unsupported", ); expect(kinds([delivered])).toEqual([["message.completed"]]); - await context.logger.destroy(); }); test("rejects an oversized lossless batch before reading its payloads", async () => { @@ -383,7 +320,6 @@ describe("DriverEventPublisher", () => { ), ).rejects.toThrow("exceeds 1024 events"); expect(payloadReads).toBe(0); - await context.logger.destroy(); }); test("rejects a receipt for a later same-kind event", async () => { @@ -419,7 +355,6 @@ describe("DriverEventPublisher", () => { await expect(publisher.push(context, "mismatched", firstBatch)).rejects.toThrow("event ID"); await publisher.push(context, "retry", [createEvent("message.started")]); expect(attempt).toBe(2); - await context.logger.destroy(); }); test("gives a reused draft object a new identity after a successful push", async () => { @@ -441,7 +376,6 @@ describe("DriverEventPublisher", () => { expect(attempts[1]?.[0]?.sourceEventId).not.toBe(attempts[0]?.[0]?.sourceEventId); expect(isDriverId(attempts[0]?.[0]?.sourceEventId)).toBe(true); expect(isDriverId(attempts[1]?.[0]?.sourceEventId)).toBe(true); - await context.logger.destroy(); }); test.each([ @@ -503,7 +437,6 @@ describe("DriverEventPublisher", () => { expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); expect(attempts[1]?.[1]?.sourceEventId).toBe(attempts[0]?.[1]?.sourceEventId); expect(publisher.lastAcceptedSeq()).toBe(52); - await context.logger.destroy(); }, ); }); diff --git a/tests/driver-event-publisher-fixture.ts b/tests/driver-event-publisher-fixture.ts new file mode 100644 index 0000000..cd7aa26 --- /dev/null +++ b/tests/driver-event-publisher-fixture.ts @@ -0,0 +1,51 @@ +import { createAgentDriverContext } from "../src/core/agent-driver-backend"; +import { createDisabledLogger } from "../src/observability"; +import type { DriverEventInput } from "../src/protocol/events"; +import type { RunId } from "../src/protocol/id"; +import type { DriverEventBatchOutput } from "../src/protocol/orpc"; +import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; +import { bootPayload } from "./driver-runtime-boundary-fixtures"; + +export function createEvent(kind: "message.started" | "message.completed"): DriverEventInput { + return { + kind, + payload: { + messageId: "message-1", + ...(kind === "message.started" ? { role: "agent" } : { stopReason: "end_turn" }), + }, + }; +} + +export function createDelta(contentDelta: string): DriverEventInput { + return { + delivery: "best_effort", + kind: "message.delta", + payload: { + contentDelta, + messageId: "message-1", + role: "agent", + }, + }; +} + +export function kinds(batches: readonly (readonly DriverEventInput[])[]): string[][] { + return batches.map((batch) => batch.map((event) => event.kind)); +} + +export function createContext(input: { + currentRunId?: () => RunId | null; + pushEvents: (events: DriverEventInput[], signal?: AbortSignal) => Promise; +}) { + return createAgentDriverContext({ + eventSink: { + commandUpdate: async () => {}, + currentRunId: input.currentRunId ?? (() => DRIVER_TEST_IDS.runId), + pushEvents: async ({ events, signal }) => input.pushEvents(events, signal), + }, + logger: createDisabledLogger(), + payload: bootPayload, + permission: { + request: async () => "reject_once", + }, + }); +} diff --git a/tests/driver-heartbeat-loop.test.ts b/tests/driver-heartbeat-loop.test.ts index 1acc844..2931d81 100644 --- a/tests/driver-heartbeat-loop.test.ts +++ b/tests/driver-heartbeat-loop.test.ts @@ -2,20 +2,13 @@ import { describe, expect, test } from "bun:test"; import { DriverHeartbeatLoop } from "../src/core/driver-heartbeat-loop"; import type { DriverRuntimeHeartbeatPort } from "../src/core/driver-runtime-io"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import { promiseWithTimeout, sleepPromise } from "../src/utils/async"; -function createTestLogger() { - return createBufferedSinkLogger({ - level: "debug", - service: "driver-heartbeat-loop-test", - sink: async () => {}, - }); -} +const logger = createDisabledLogger(); describe("DriverHeartbeatLoop", () => { test("clamps a huge negotiated interval only when scheduling the timer", async () => { - const logger = createTestLogger(); const scheduledDelays: number[] = []; const nativeSetTimeout = globalThis.setTimeout; const loop = new DriverHeartbeatLoop({ @@ -43,13 +36,11 @@ describe("DriverHeartbeatLoop", () => { } finally { loop.stop(logger, "test.complete"); globalThis.setTimeout = nativeSetTimeout; - await logger.destroy(); } }); test("never overlaps heartbeat requests", async () => { const heartbeat = Promise.withResolvers<{ heartbeatCount: number; ok: true }>(); - const logger = createTestLogger(); let heartbeatCalls = 0; const socket: DriverRuntimeHeartbeatPort = { heartbeat: async () => { @@ -67,14 +58,12 @@ describe("DriverHeartbeatLoop", () => { loop.stop(logger, "test.complete"); heartbeat.resolve({ heartbeatCount: 1, ok: true }); await sleepPromise(0); - await logger.destroy(); expect(heartbeatCalls).toBe(1); }); test("reports one failed heartbeat to the supervisor and stops", async () => { const failed = Promise.withResolvers(); - const logger = createTestLogger(); let heartbeatCalls = 0; const socket: DriverRuntimeHeartbeatPort = { heartbeat: async () => { @@ -93,7 +82,6 @@ describe("DriverHeartbeatLoop", () => { timeoutMs: 100, }); await sleepPromise(10); - await logger.destroy(); expect(error).toBeInstanceOf(Error); expect(heartbeatCalls).toBe(1); @@ -104,7 +92,6 @@ describe("DriverHeartbeatLoop", () => { async (termination) => { const heartbeatEntered = Promise.withResolvers(); const heartbeatResult = Promise.withResolvers<{ heartbeatCount: number; ok: true }>(); - const logger = createTestLogger(); const failures: unknown[] = []; let shuttingDown = false; const loop = new DriverHeartbeatLoop({ @@ -130,7 +117,6 @@ describe("DriverHeartbeatLoop", () => { heartbeatResult.reject(new Error("late heartbeat failure")); await sleepPromise(0); loop.stop(logger, "test.cleanup"); - await logger.destroy(); expect(failures).toEqual([]); }, diff --git a/tests/driver-instance-socket-process.test.ts b/tests/driver-instance-socket-process.test.ts index 82cebb0..20be177 100644 --- a/tests/driver-instance-socket-process.test.ts +++ b/tests/driver-instance-socket-process.test.ts @@ -947,102 +947,6 @@ describe("DriverProcess lifecycle", () => { expect(socket.paths.filter((path) => path === "/driver/failRun")).toHaveLength(1); }); - test.each([ - ["transient", 1, "completed"], - ["persistent", Number.POSITIVE_INFINITY, "failed"], - ] as const)( - "bounds and retries a %s backend cleanup failure during process shutdown", - async (_name, failures, expectedStatus) => { - globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; - RpcWebSocket.stalledPath = "/driverInstance/nextCommand"; - const existingSignalListeners = new Set(process.listeners("SIGTERM")); - const backend = createBackend(); - let stopCount = 0; - backend.stop = async () => { - stopCount += 1; - - if (stopCount <= failures) { - throw new Error("cleanup failed"); - } - }; - const driver = new DriverProcess(driverBootPayload, () => backend); - const run = driver.run(); - - await RpcWebSocket.stalled.promise; - const shutdown = process - .listeners("SIGTERM") - .find((listener) => !existingSignalListeners.has(listener)); - expect(shutdown).toBeDefined(); - shutdown?.("SIGTERM"); - - const outcome = await settlePromiseWithTimeout(run, { - label: "driver process cleanup retry", - timeoutMs: 1_000, - }); - - expect(outcome.status).toBe(expectedStatus); - expect(stopCount).toBe(2); - expect(process.listeners("SIGTERM")).toEqual([...existingSignalListeners]); - }, - ); - - test("starts a new backend cleanup attempt after the previous one times out", async () => { - globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; - RpcWebSocket.stalledPath = "/driverInstance/nextCommand"; - const existingSignalListeners = new Set(process.listeners("SIGTERM")); - const firstStopAborted = Promise.withResolvers(); - const backend = createBackend(); - const stopSignals: AbortSignal[] = []; - let stopCount = 0; - backend.stop = async (_context, _reason, signal) => { - stopCount += 1; - stopSignals.push(signal); - - if (stopCount === 1) { - await new Promise((_resolve, reject) => { - signal.addEventListener( - "abort", - () => { - firstStopAborted.resolve(); - reject(signal.reason); - }, - { once: true }, - ); - }); - } - }; - const nativeNow = Date.now; - const nativeSetTimeout = globalThis.setTimeout; - const acceleratedSetTimeout = ( - callback: (...args: unknown[]) => void, - delay?: number, - ...args: unknown[] - ) => nativeSetTimeout(callback, delay === 5_000 ? 10 : delay, ...args); - Date.now = () => 0; - globalThis.setTimeout = acceleratedSetTimeout as typeof setTimeout; - - try { - const run = new DriverProcess(driverBootPayload, () => backend).run(); - await RpcWebSocket.stalled.promise; - const shutdown = process - .listeners("SIGTERM") - .find((listener) => !existingSignalListeners.has(listener)); - expect(shutdown).toBeDefined(); - shutdown?.("SIGTERM"); - - await expect(run).resolves.toBeUndefined(); - await firstStopAborted.promise; - expect(stopCount).toBe(2); - expect(stopSignals[0]?.aborted).toBe(true); - expect(stopSignals[1]).not.toBe(stopSignals[0]); - expect(stopSignals[1]?.aborted).toBe(false); - expect(process.listeners("SIGTERM")).toEqual([...existingSignalListeners]); - } finally { - Date.now = nativeNow; - globalThis.setTimeout = nativeSetTimeout; - } - }); - test("retries a shutdown failure joined by process finalization", async () => { globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; RpcWebSocket.stalledPath = "/driverInstance/nextCommand"; @@ -1069,8 +973,6 @@ describe("DriverProcess lifecycle", () => { expect(shutdown).toBeDefined(); shutdown?.("SIGTERM"); await firstStopEntered.promise; - await Bun.sleep(10); - expect(stopCount).toBe(1); releaseFirstStop.resolve(); const outcome = await settlePromiseWithTimeout(run, { @@ -1083,6 +985,33 @@ describe("DriverProcess lifecycle", () => { expect(process.listeners("SIGTERM")).toEqual([...existingSignalListeners]); }); + test("reports a persistent cleanup failure during signal shutdown", async () => { + globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; + RpcWebSocket.stalledPath = "/driverInstance/nextCommand"; + const existingSignalListeners = new Set(process.listeners("SIGTERM")); + const backend = createBackend(); + let stopCount = 0; + backend.stop = async () => { + stopCount += 1; + throw new Error("cleanup failed"); + }; + const run = new DriverProcess(driverBootPayload, () => backend).run(); + + await RpcWebSocket.stalled.promise; + process.listeners("SIGTERM").find((listener) => !existingSignalListeners.has(listener))?.( + "SIGTERM", + ); + + const outcome = await settlePromiseWithTimeout(run, { + label: "driver process persistent cleanup failure", + timeoutMs: 1_000, + }); + + expect(outcome).toMatchObject({ error: { message: "cleanup failed" }, status: "failed" }); + expect(stopCount).toBe(2); + expect(process.listeners("SIGTERM")).toEqual([...existingSignalListeners]); + }); + test.each([ ["transient", 1], ["persistent", Number.POSITIVE_INFINITY], diff --git a/tests/driver-runtime-boundary-dispatcher.test.ts b/tests/driver-runtime-boundary-dispatcher.test.ts index 2642f6f..bf05428 100644 --- a/tests/driver-runtime-boundary-dispatcher.test.ts +++ b/tests/driver-runtime-boundary-dispatcher.test.ts @@ -66,7 +66,6 @@ describe("driver runtime boundary", () => { expect(socket.completedRunReasons).toEqual([]); releaseCleanup.resolve(); await run; - await logger.destroy(); expect(terminalAttempts).toBe(3); expect(order).toEqual(["cleanup", "control.completeRun"]); @@ -152,7 +151,6 @@ describe("driver runtime boundary", () => { await Bun.sleep(5_100); releaseRequested.resolve(); await expect(Promise.all([permission!, run])).resolves.toEqual(["reject_once", undefined]); - await logger.destroy(); expect( order.filter((kind) => ["permission.resolved", "run.cancelled", "cleanup", "control.completeRun"].includes(kind), @@ -204,7 +202,6 @@ describe("driver runtime boundary", () => { }); await expect(dispatcher.run(socket, logger)).resolves.toBeUndefined(); - await logger.destroy(); expect(socket.failedRuns).toHaveLength(1); expect(socket.completedRunReasons).toEqual([]); @@ -272,7 +269,6 @@ describe("driver runtime boundary", () => { }); await expect(dispatcher.run(socket, logger)).resolves.toBeUndefined(); - await logger.destroy(); expect(socket.failedRuns).toHaveLength(1); expect( @@ -342,7 +338,6 @@ describe("driver runtime boundary", () => { await Bun.sleep(0); releaseCancelAccepted.resolve(); await run; - await logger.destroy(); expect(cancellationRequested).toBe(true); expect(sideEffects).toBe(0); @@ -379,7 +374,6 @@ describe("driver runtime boundary", () => { }); await expect(dispatcher.run(socket, logger)).resolves.toBeUndefined(); - await logger.destroy(); expect(socket.failedRuns).toEqual([]); expect(runtimeState.status()).toBe("failed"); @@ -412,7 +406,6 @@ describe("driver runtime boundary", () => { }); await expect(dispatcher.run(socket, logger)).resolves.toBeUndefined(); - await logger.destroy(); expect(cleanupAttempts).toBe(2); expect(socket.failedRuns).toHaveLength(1); @@ -475,7 +468,6 @@ describe("driver runtime boundary", () => { }); await dispatcher.run(socket, logger); - await logger.destroy(); expect(attempts).toBe(2); expect(socket.completedRunReasons).toHaveLength(runStatus === "completed" ? 1 : 0); @@ -527,7 +519,6 @@ describe("driver runtime boundary", () => { label: `session stop run terminal that ${failureMode}`, timeoutMs: 1_500, }); - await logger.destroy(); expect(outcome.status).toBe("completed"); expect(attempts).toBe(3); @@ -598,7 +589,6 @@ describe("driver runtime boundary", () => { await expect(dispatcher.run(socket, logger)).rejects.toThrow( "terminal status could not be delivered", ); - await logger.destroy(); expect(terminalAttempts).toBe(3); expect(runtimeState.status()).toBe("failed"); @@ -646,7 +636,6 @@ describe("driver runtime boundary", () => { await expect(dispatcher.run(socket, logger)).rejects.toThrow( "terminal status could not be delivered", ); - await logger.destroy(); expect(terminalAttempts).toBe(1); expect(socket.completedRunReasons).toHaveLength(1); @@ -741,7 +730,6 @@ describe("driver runtime boundary", () => { label: `${kind} slow terminal acknowledgement`, timeoutMs: 1_500, }); - await logger.destroy(); expect(outcome.status).toBe("completed"); expect(terminalAttempts).toBe(1); @@ -766,7 +754,6 @@ describe("driver runtime boundary", () => { }); await expect(dispatcher.run(socket, logger)).rejects.toThrow("history capacity"); - await logger.destroy(); expect(backend.cancelledReasons).toHaveLength(1_024); expect(backend.cancelledReasons[0]).toBe("reason-0"); @@ -790,7 +777,6 @@ describe("driver runtime boundary", () => { }); await dispatcher.run(socket, logger); - await logger.destroy(); expect(runtimeState.status()).toBe("stopped"); expect(socket.completedRunReasons).toEqual(["completed"]); diff --git a/tests/driver-runtime-boundary-event-envelope.test.ts b/tests/driver-runtime-boundary-event-envelope.test.ts index c50bb33..d61ead6 100644 --- a/tests/driver-runtime-boundary-event-envelope.test.ts +++ b/tests/driver-runtime-boundary-event-envelope.test.ts @@ -84,7 +84,6 @@ describe("driver runtime boundary", () => { expect(terminalStarted).toBe(false); releaseAccepted.resolve(); await run; - await logger.destroy(); expect(sideEffects).toBe(1); expect(socket.updates.map((update) => update.status)).toEqual([ @@ -174,7 +173,6 @@ describe("driver runtime boundary", () => { expect(socket.updates.filter((update) => update.status === "accepted")).toHaveLength(1); releaseTerminal.resolve(); await run; - await logger.destroy(); expect(sideEffects).toBe(1); expect(terminalAttempts).toBe(2); @@ -276,7 +274,6 @@ describe("driver runtime boundary", () => { label: `${kind} shared terminal failure`, timeoutMs: 1_500, }); - await logger.destroy(); expect(attemptsBeforeRelease).toBe(1); expect(outcome).toMatchObject({ @@ -377,7 +374,6 @@ describe("driver runtime boundary", () => { await terminalEntered.promise; releaseTerminal.resolve(); await run; - await logger.destroy(); expect(executeCalls).toBe(1); expect(terminalSnapshots).toHaveLength(2); @@ -446,7 +442,6 @@ describe("driver runtime boundary", () => { }); await dispatcher.run(socket, logger); - await logger.destroy(); expect(sideEffects).toBe(1); expect(terminalAttempts).toEqual(["completed", "completed", "completed"]); diff --git a/tests/driver-runtime-boundary-fixtures.ts b/tests/driver-runtime-boundary-fixtures.ts index 8303474..b452113 100644 --- a/tests/driver-runtime-boundary-fixtures.ts +++ b/tests/driver-runtime-boundary-fixtures.ts @@ -5,7 +5,7 @@ import { DriverPermissionBroker } from "../src/core/driver-permission-broker"; import type { DriverRuntimeIo } from "../src/core/driver-runtime-io"; import type { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; import type { AgentDriverMcpPort } from "../src/host-ports"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; import type { McpExecuteCommand, @@ -170,11 +170,7 @@ export function createDispatcher(input: { shutdownSignal?: AbortSignal; shutdown?: (socket: DriverRuntimeIo, reason: string) => Promise; }) { - const logger = createBufferedSinkLogger({ - level: "debug", - service: "driver-runtime-boundary-test", - sink: async () => {}, - }); + const logger = createDisabledLogger(); const commandReads = { count: 0, }; diff --git a/tests/driver-runtime-boundary-state.test.ts b/tests/driver-runtime-boundary-state.test.ts index 8c99681..b8f0096 100644 --- a/tests/driver-runtime-boundary-state.test.ts +++ b/tests/driver-runtime-boundary-state.test.ts @@ -326,9 +326,11 @@ describe("driver runtime boundary", () => { await dispatcher.run(socket, logger); await waitForUpdate( socket, - (update) => update.commandId === "input-1" && update.status === "completed", + (update) => + update.commandId === "input-1" && + update.status === "completed" && + runtimeState.status() === "ready", ); - await logger.destroy(); expect(runtimeState.status()).toBe("ready"); expect(commandReads.count).toBe(1); @@ -415,7 +417,6 @@ describe("driver runtime boundary", () => { (update) => update.commandId === command.commandId && update.status === "completed", ); await runTask; - await logger.destroy(); expect(calls).toBe(1); expect(socket.updates.filter((update) => update.status === "accepted")).toHaveLength(2); @@ -490,7 +491,6 @@ describe("driver runtime boundary", () => { await first.dispatcher.run(firstSocket, first.logger); await effectPersisted.promise; - await first.logger.destroy(); const secondSocket = new FakeDriverRuntimeIo([structuredClone(command)]); let secondClaims = 0; @@ -521,7 +521,6 @@ describe("driver runtime boundary", () => { }; }; await second.dispatcher.run(secondSocket, second.logger); - await second.logger.destroy(); expect(providerCalls).toBe(1); expect(secondPreparations).toBe(1); @@ -578,7 +577,10 @@ describe("driver runtime boundary", () => { }); await runtime.dispatcher.run(socket, runtime.logger); - await runtime.logger.destroy(); + await waitForUpdate( + socket, + (update) => update.commandId === command.commandId && update.status === "failed", + ); expect(preparations).toBe(1); expect(claims).toBe(1); @@ -640,7 +642,6 @@ describe("driver runtime boundary", () => { await runtime.dispatcher.run(socket, runtime.logger); await disposed.promise; - await runtime.logger.destroy(); expect(executions).toBe(0); expect(disposals).toBe(1); @@ -742,7 +743,6 @@ describe("driver runtime boundary", () => { socket, (update) => update.commandId === command.commandId && update.status === "completed", ); - await runtime.logger.destroy(); expect(providerCalls).toBe(settlement === "executed" ? 1 : 0); expect(socket.unknownCount).toBe(0); @@ -815,7 +815,6 @@ describe("driver runtime boundary", () => { socket, (update) => update.commandId === command.commandId && update.status === "failed", ); - await runtime.logger.destroy(); const failure = socket.updates.find( (update) => update.commandId === command.commandId && update.status === "failed", ); @@ -881,7 +880,6 @@ describe("driver runtime boundary", () => { expect(socket.claimCount).toBe(0); expect(socket.unknownCount).toBe(0); } finally { - await runtime.logger.destroy(); await server.stop(true); } }); @@ -962,7 +960,6 @@ describe("driver runtime boundary", () => { shutdown.abort(new Error("test cancellation")); releaseBoundary.resolve(); await runTask; - await runtime.logger.destroy(); expect(preparations).toBe(stage === "before prepare" ? 0 : 1); expect(executions).toBe(0); @@ -1016,7 +1013,6 @@ describe("driver runtime boundary", () => { socket, (update) => update.commandId === command.commandId && update.status === "failed", ); - await runtime.logger.destroy(); expect(socket.unknownCount).toBe(1); expect(socket.fenceSignalAborted).toBe(false); @@ -1094,7 +1090,6 @@ describe("driver runtime boundary", () => { socket.stop(); shutdown.abort(new Error("test shutdown")); await runTask; - await runtime.logger.destroy(); expect(socket.fenceSignalAborted).toBe(false); expect(socket.fencedCommandId).toBe(mcpCommand.commandId); @@ -1190,7 +1185,6 @@ describe("driver runtime boundary", () => { shutdown.abort(new Error("test shutdown")); releaseProvider.resolve(); await runTask; - await runtime.logger.destroy(); expect(socket.completionAttempts).toBe(2); expect(socket.completionSignals).toEqual([false, false]); @@ -1238,7 +1232,6 @@ describe("driver runtime boundary", () => { await expect(dispatcher.run(socket, logger)).rejects.toThrow( "replayed with changed identity or content", ); - await logger.destroy(); expect(backend.cancelledReasons).toEqual(["first reason"]); expect(socket.updates).toEqual([ @@ -1302,7 +1295,13 @@ describe("driver runtime boundary", () => { ); firstInputCanFinish.resolve(); await runTask; - await logger.destroy(); + await waitForUpdate( + socket, + (update) => + update.commandId === "input-2" && + update.status === "completed" && + runtimeState.status() === "ready", + ); expect(handledInputCount).toBe(2); expect(runtimeState.status()).toBe("ready"); diff --git a/tests/driver-runtime-boundary-timing.test.ts b/tests/driver-runtime-boundary-timing.test.ts index bc94956..9598e13 100644 --- a/tests/driver-runtime-boundary-timing.test.ts +++ b/tests/driver-runtime-boundary-timing.test.ts @@ -95,7 +95,6 @@ describe("driver runtime boundary", () => { }); } finally { globalThis.setTimeout = nativeSetTimeout; - await logger.destroy(); } }); @@ -133,7 +132,6 @@ describe("driver runtime boundary", () => { label: "externally stopped command loop", timeoutMs: 100, }); - await logger.destroy(); expect(outcome.status).toBe("completed"); expect(socket.updates).toContainEqual({ @@ -201,7 +199,6 @@ describe("driver runtime boundary", () => { pending.reject(new Error("late transport failure")); } await Bun.sleep(0); - await logger.destroy(); expect(socket.failedRuns).toEqual([]); }, @@ -262,7 +259,6 @@ describe("driver runtime boundary", () => { late.reject(new Error("late accepted ACK failure")); } await Bun.sleep(0); - await logger.destroy(); }, ); @@ -290,7 +286,6 @@ describe("driver runtime boundary", () => { }); await dispatcher.run(socket, logger); - await logger.destroy(); expect(runtimeState.status()).toBe("ready"); expect(commandReads.count).toBeGreaterThanOrEqual(1); @@ -364,7 +359,6 @@ describe("driver runtime boundary", () => { }); await dispatcher.run(socket, logger); - await logger.destroy(); expect(socket.updates).toMatchObject([ { @@ -476,7 +470,6 @@ describe("driver runtime boundary", () => { socket, (update) => update.commandId === "mcp-stuck" && update.status === "cancelled", ); - await logger.destroy(); expect(runtimeState.status()).toBe("stopped"); expect(shutdownCalls).toEqual(["test.stop"]); @@ -520,7 +513,6 @@ describe("driver runtime boundary", () => { updateResult.reject(new Error("shutdown abort")); await expect(run).resolves.toBeUndefined(); - await logger.destroy(); expect(socket.failedRuns).toEqual([]); expect(socket.pushedEvents).toEqual([]); expect(shutdownCalls).toEqual([]); @@ -564,7 +556,6 @@ describe("driver runtime boundary", () => { socket, (update) => update.commandId === "mcp-32" && update.status === "failed", ); - await logger.destroy(); expect(executeCalls).toBe(32); expect(runtimeState.status()).toBe("stopped"); @@ -600,7 +591,6 @@ describe("driver runtime boundary", () => { socket, (update) => update.commandId === "input-1" && update.status === "failed", ); - await logger.destroy(); expect(runtimeState.status()).toBe("failed"); expect(socket.failedRuns).toHaveLength(1); diff --git a/tests/openai-app-server-auth-state.test.ts b/tests/openai-app-server-auth-state.test.ts index 388aee2..aa45b6d 100644 --- a/tests/openai-app-server-auth-state.test.ts +++ b/tests/openai-app-server-auth-state.test.ts @@ -215,8 +215,9 @@ describe("OpenAI app-server auth state", () => { expect(config["mcp_servers"]).toBeUndefined(); }); - test("skips unchanged generated config writes", async () => { + test("skips unchanged regular config writes but replaces an unchanged symlink", async () => { const runtimeHome = await createRuntimeHome(); + const configTomlPath = join(runtimeHome, "config.toml"); const input = { env: { OPENAI_API_KEY: "openai-key", @@ -231,6 +232,18 @@ describe("OpenAI app-server auth state", () => { await expect(materializeOpenAiModelProviderConfig(input)).resolves.toMatchObject({ written: false, }); + + const contents = await readFile(configTomlPath, "utf8"); + const target = join(runtimeHome, "config-target.toml"); + await rm(configTomlPath); + await writeFile(target, contents); + await symlink(target, configTomlPath); + + await expect(materializeOpenAiModelProviderConfig(input)).resolves.toMatchObject({ + written: true, + }); + expect((await lstat(configTomlPath)).isSymbolicLink()).toBe(false); + expect(await readFile(target, "utf8")).toBe(contents); }); test("fails OpenAI-compatible provider config when credentials are incomplete", async () => { diff --git a/tests/openai-app-server-client.test.ts b/tests/openai-app-server-client.test.ts index dbd0073..58840a2 100644 --- a/tests/openai-app-server-client.test.ts +++ b/tests/openai-app-server-client.test.ts @@ -7,7 +7,7 @@ import { join } from "node:path"; import { Readable } from "node:stream"; import type { AgentDriverPermissionPort } from "../src/host-ports"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { DriverExecutionEnvironment } from "../src/protocol/boot"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; @@ -58,17 +58,12 @@ async function createClientHarness( }, }, }); - const logger = createBufferedSinkLogger({ - level: "debug", - service: "openai-app-server-client-test", - sink: async () => {}, - }); const context = createAgentDriverContext({ eventSink: { currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload, permission: { request: requestPermission }, }); @@ -84,7 +79,7 @@ async function createClientHarness( mapToolCallId: (toolCallId) => toolCallId, }); - return { client, directory, logger, protocolError: protocolError.promise, protocolErrors }; + return { client, directory, protocolError: protocolError.promise, protocolErrors }; } afterEach(async () => { @@ -147,7 +142,6 @@ setInterval(() => {}, 1000); ).toEqual({ method: "initialized" }); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -194,7 +188,6 @@ setInterval(() => {}, 1000); expect(harness.protocolErrors).toHaveLength(1); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -238,7 +231,6 @@ setInterval(() => {}, 1000); expect(harness.protocolErrors[0]?.message).toContain(message); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -285,7 +277,6 @@ setInterval(() => {}, 1000); } finally { notificationGate.resolve(); await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -314,7 +305,6 @@ setInterval(() => {}, 1000); expect(harness.protocolErrors[0]?.message).toContain("response envelope is invalid"); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -338,7 +328,6 @@ setInterval(() => {}, 1000); expect(harness.protocolErrors).toHaveLength(1); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -385,7 +374,6 @@ setInterval(() => {}, 1000); expect(handled).toEqual([]); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -447,7 +435,6 @@ setInterval(() => {}, 1000); await permissionAborted.promise; } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -469,7 +456,6 @@ setInterval(() => {}, 1000); expect(harness.protocolErrors).toEqual([]); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -494,7 +480,6 @@ setInterval(() => {}, 1000); ).rejects.toThrow("stopping"); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -531,7 +516,6 @@ setInterval(() => {}, 1000); expect(harness.protocolErrors).toHaveLength(1); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -548,7 +532,6 @@ setInterval(() => {}, 1000); await expect(harness.client.stop()).resolves.toBeUndefined(); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }, ); @@ -612,7 +595,6 @@ setInterval(() => {}, 1000); } catch {} } await harness.client.stop().catch(() => {}); - await harness.logger.destroy(); } }, ); @@ -635,7 +617,6 @@ setInterval(() => {}, 1000); expect(await Bun.file(join(harness.directory, "spawned")).exists()).toBe(false); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -678,7 +659,6 @@ setTimeout(() => process.exit(0), 500); ).toHaveLength(1); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -718,7 +698,6 @@ setInterval(() => {}, 1000); await expect(stop).resolves.toBeUndefined(); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -762,7 +741,6 @@ process.stdin.on("data", (chunk) => { expect(harness.protocolErrors[0]?.message).toBe("OpenAi app-server exited with code 17."); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -842,7 +820,6 @@ setInterval(() => {}, 1000); } finally { permissionGate.resolve(); await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -931,7 +908,6 @@ setInterval(() => {}, 1000); } finally { permissionGate.resolve(); await harness.client.stop().catch(() => {}); - await harness.logger.destroy(); } }); @@ -983,7 +959,6 @@ setInterval(() => {}, 1000); expect(protocolError.message).toContain("code 17"); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -1037,7 +1012,6 @@ setInterval(() => {}, 1000); } finally { permissionGate.resolve(); await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -1088,7 +1062,6 @@ process.stdin.on("data", (chunk) => { } finally { terminalGate.resolve(); await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -1152,7 +1125,6 @@ process.stdin.on("data", (chunk) => { watchdogCleanup.resolve(); watchdogSpy.mockRestore(); await harness.client.stop().catch(() => {}); - await harness.logger.destroy(); } }, ); @@ -1189,7 +1161,6 @@ sleep 10 expect(harness.protocolErrors[0]?.message).toContain("EPIPE"); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -1237,7 +1208,6 @@ setInterval(() => {}, 1000); expect(harness.protocolErrors[0]?.message).toContain(expectedExit); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }, ); @@ -1311,7 +1281,6 @@ setInterval(() => {}, 1000); notificationGate.resolve(); secondNotificationGate.resolve(); await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -1347,7 +1316,6 @@ setInterval(() => {}, 1000); await expect(harness.client.start()).rejects.toThrow("cannot be started more than once"); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -1403,7 +1371,6 @@ process.stdin.on("data", (chunk) => { } await harness.client.stop().catch(() => {}); - await harness.logger.destroy(); } }, 10_000); @@ -1466,7 +1433,6 @@ setInterval(() => {}, 1000); } await harness.client.stop().catch(() => {}); - await harness.logger.destroy(); } }, 10_000); @@ -1546,7 +1512,6 @@ setInterval(() => {}, 1000); } await harness.client.stop().catch(() => {}); - await harness.logger.destroy(); } }, 10_000); @@ -1609,8 +1574,6 @@ setInterval(() => {}, 1000); nativeKill(descendantPid, "SIGKILL"); } catch {} } - - await harness.logger.destroy(); } }, 10_000); @@ -1654,7 +1617,6 @@ setInterval(() => {}, 1000); }); } finally { await harness.client.stop(); - await harness.logger.destroy(); } }); @@ -1728,7 +1690,6 @@ setInterval(() => {}, 1000); } finally { notificationGate.resolve(); await harness.client.stop(); - await harness.logger.destroy(); } }); }); diff --git a/tests/openai-app-server-event-bridge-fixture.ts b/tests/openai-app-server-event-bridge-fixture.ts index cc010b0..19ce551 100644 --- a/tests/openai-app-server-event-bridge-fixture.ts +++ b/tests/openai-app-server-event-bridge-fixture.ts @@ -2,7 +2,7 @@ import { expect } from "bun:test"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; @@ -60,17 +60,12 @@ export function createOpenAiBridgeHarness( const releasePush = Promise.withResolvers(); const terminalHeld = Promise.withResolvers(); const releaseTerminal = Promise.withResolvers(); - const logger = createBufferedSinkLogger({ - level: "debug", - service: "openai-app-server-event-bridge-test", - sink: async () => {}, - }); const context: AgentDriverContext = createAgentDriverContext({ eventSink: { currentRunId: () => DRIVER_TEST_IDS.runId, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload: bootPayload, permission: { request: async () => "allow_once", @@ -122,7 +117,6 @@ export function createOpenAiBridgeHarness( context, events: () => batches.flatMap((batch) => batch.events), heldPush: heldPush.promise, - logger, releasePush: releasePush.resolve, releaseTerminal: releaseTerminal.resolve, terminalHeld: terminalHeld.promise, diff --git a/tests/openai-app-server-event-bridge-items.test.ts b/tests/openai-app-server-event-bridge-items.test.ts index 79b0d35..3ae2927 100644 --- a/tests/openai-app-server-event-bridge-items.test.ts +++ b/tests/openai-app-server-event-bridge-items.test.ts @@ -106,7 +106,7 @@ describe("OpenAi app-server event bridge", () => { }); test("closes visible message, tool, and run state when a turn is cancelled", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "item/started", { @@ -143,11 +143,10 @@ describe("OpenAi app-server event bridge", () => { turnId: "turn-1", }); expect(events()).toHaveLength(terminalEventCount); - await logger.destroy(); }); test("accepts only a completed sub-agent activity after its parent turn terminal", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "turn/completed", { @@ -191,11 +190,10 @@ describe("OpenAi app-server event bridge", () => { turnId: "turn-1", }); expect(events()).toHaveLength(terminalEventCount + 1); - await logger.destroy(); }); test("closes open item state when the provider interrupts a turn", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "item/started", { @@ -219,11 +217,10 @@ describe("OpenAi app-server event bridge", () => { runId: DRIVER_TEST_IDS.runId, }, ]); - await logger.destroy(); }); test("closes open item state before a failed turn", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "item/started", { @@ -252,11 +249,10 @@ describe("OpenAi app-server event bridge", () => { { kind: "item.completed", payload: { itemId: "tool-1", status: "failed" } }, { kind: "run.failed", runId: DRIVER_TEST_IDS.runId }, ]); - await logger.destroy(); }); test("closes open item state before a successful turn without loaded items", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "item/started", { @@ -277,7 +273,6 @@ describe("OpenAi app-server event bridge", () => { { kind: "item.completed", payload: { itemId: "tool-1", status: "completed" } }, { kind: "run.completed", runId: DRIVER_TEST_IDS.runId }, ]); - await logger.destroy(); }); test("drains pending item delivery before cancellation and leaves the next turn clean", async () => { @@ -344,7 +339,6 @@ describe("OpenAi app-server event bridge", () => { event.kind === "item.started" && readEventPayloadString(event, "itemId") === "tool-1", ), ).toHaveLength(2); - await harness.logger.destroy(); }); test.each([ @@ -352,7 +346,7 @@ describe("OpenAi app-server event bridge", () => { ["failed", "failed"], ["declined", "failed"], ] as const)("maps completed native tool status %s to %s", async (nativeStatus, status) => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/completed", { item: { @@ -373,7 +367,6 @@ describe("OpenAi app-server event bridge", () => { expect(events().find((event) => event.kind === "item.completed")?.payload).toMatchObject({ status, }); - await logger.destroy(); }); test("replays an item completion after delivery rejection before committing state", async () => { @@ -407,7 +400,6 @@ describe("OpenAi app-server event bridge", () => { .events() .filter((event) => event.kind === "message.added" || event.kind === "message.completed"), ).toHaveLength(2); - await harness.logger.destroy(); }); test("replays a plan delta after delivery rejection without duplicating content", async () => { @@ -444,7 +436,6 @@ describe("OpenAi app-server event bridge", () => { .map((batch) => batch.events[0]?.sourceEventId), ).toEqual(["openai.plan.delta:turn-1:plan-1:0", "openai.plan.delta:turn-1:plan-1:0"]); expect(JSON.stringify(accepted)).not.toContain("stepstep"); - await harness.logger.destroy(); }); test("retries message start with the same identity after delivery rejection", async () => { @@ -477,7 +468,6 @@ describe("OpenAi app-server event bridge", () => { .filter((event) => event.kind === "message.delta") .map((event) => readEventPayloadString(event, "contentDelta")), ).toEqual(["hello"]); - await harness.logger.destroy(); }); test("replays terminal closures after rejection before settling the turn", async () => { @@ -509,7 +499,6 @@ describe("OpenAi app-server event bridge", () => { .map((event) => readEventPayloadString(event, "status")), ).toEqual(["running", "completed"]); expect(harness.events().filter((event) => event.kind === "run.completed")).toHaveLength(1); - await harness.logger.destroy(); }); test("replays cancellation closures after rejection before rejecting the turn", async () => { @@ -531,7 +520,6 @@ describe("OpenAi app-server event bridge", () => { expect(harness.terminalAttempts).toHaveLength(2); expect(harness.terminalAttempts[0]!.closures).toEqual(harness.terminalAttempts[1]!.closures); expect(harness.events().filter((event) => event.kind === "run.cancelled")).toHaveLength(1); - await harness.logger.destroy(); }); test("waits for a failed concurrent settlement before claiming cancellation", async () => { @@ -566,11 +554,10 @@ describe("OpenAi app-server event bridge", () => { "run.completed", "run.cancelled", ]); - await harness.logger.destroy(); }); test("orders cancellation after an in-flight item update", async () => { - const { bridge, context, events, heldPush, logger, releasePush } = createHarness({ + const { bridge, context, events, heldPush, releasePush } = createHarness({ holdReason: "driver.openai.message.started", }); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); @@ -602,11 +589,10 @@ describe("OpenAi app-server event bridge", () => { readEventPayloadString(event, "status") === "running", ), ).toBeLessThan(events().findLastIndex((event) => event.kind === "run.cancelled")); - await logger.destroy(); }); test("maps hooks and automatic approval reviews to their exact lifecycle events", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const hookRun = { displayOrder: 0, @@ -659,11 +645,10 @@ describe("OpenAi app-server event bridge", () => { "permission.review.completed", ]); expect(events()[2]?.payload).toMatchObject({ reviewId: "review-1", targetItemId: "tool-1" }); - await logger.destroy(); }); test("preserves bounded permission profiles in automatic approval reviews", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const permissions = { fileSystem: { read: ["/workspace"], write: ["/tmp"] }, network: { enabled: true }, @@ -737,11 +722,10 @@ describe("OpenAi app-server event bridge", () => { review: { riskLevel: "high", status: "denied", userAuthorization: "high" }, }); expect(JSON.stringify(events().slice(1))).not.toContain("secret input"); - await logger.destroy(); }); test("keeps mapped MCP progress and terminal interaction visible", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const nativeItemId = `mcp-${"x".repeat(300)}`; const nativeCommandId = `command-${"y".repeat(300)}`; @@ -846,11 +830,10 @@ describe("OpenAi app-server event bridge", () => { }, }); expect(JSON.stringify(events())).not.toContain("y\\n"); - await logger.destroy(); }); test("publishes model, MCP server, and user-facing warning notifications", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await handleOfficialNotification(bridge, context, "mcpServer/startupStatus/updated", { error: null, @@ -922,6 +905,5 @@ describe("OpenAi app-server event bridge", () => { expect(worldWritableContent).toContain("C:\\unsafe-one"); expect(worldWritableContent).toContain("2 additional affected paths"); expect(worldWritableContent).toContain("scan did not complete"); - await logger.destroy(); }); }); diff --git a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts index 69ec3da..e4cbda6 100644 --- a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts +++ b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts @@ -9,7 +9,7 @@ import { describe("OpenAi app-server event bridge", () => { test("publishes a lossless completed assistant snapshot", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const finalText = "I will inspect the files."; await bridge.handleNotification(context, "item/agentMessage/delta", { @@ -27,7 +27,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-1", }); - await logger.destroy(); const assistantMessageId = readAssistantMessageId(events()); const snapshot = events().find((event) => event.kind === "message.added"); @@ -44,7 +43,7 @@ describe("OpenAi app-server event bridge", () => { }); test("preserves final phase and memory citations on completed snapshots", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const memoryCitation = { entries: [{ lineEnd: 8, lineStart: 4, note: "Relevant context", path: "MEMORY.md" }], threadIds: ["thread-memory"], @@ -61,7 +60,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-1", }); - await logger.destroy(); expect(events().find((event) => event.kind === "message.added")).toMatchObject({ payload: { memoryCitation, phase: "final" }, @@ -72,7 +70,7 @@ describe("OpenAi app-server event bridge", () => { }); test("selects only final-phase messages once a turn uses explicit phases", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "turn/completed", { threadId: "thread-1", @@ -106,7 +104,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const allEvents = events(); const finalSnapshot = allEvents.find( @@ -125,7 +122,7 @@ describe("OpenAi app-server event bridge", () => { }); test("keeps asynchronous progress out of legacy final selection", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "turn/completed", { threadId: "thread-1", @@ -151,7 +148,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const allEvents = events(); const finalSnapshot = allEvents.find( @@ -173,7 +169,7 @@ describe("OpenAi app-server event bridge", () => { }); test("does not fall back to a legacy snapshot when explicit commentary exists", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); for (const item of [ { id: "legacy", memoryCitation: null, phase: null, text: "Legacy", type: "agentMessage" }, @@ -195,7 +191,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turn: { id: "turn-1", items: [], itemsView: "notLoaded", status: "completed" }, }); - await logger.destroy(); const terminalPayload = events().find((event) => event.kind === "run.completed")?.payload; expect(terminalPayload).not.toHaveProperty("finalMessageId"); @@ -203,7 +198,7 @@ describe("OpenAi app-server event bridge", () => { }); test("fails an active turn exactly once when the provider exits", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const failure = new Error("app-server exited"); const completion = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); void completion.catch(() => {}); @@ -224,7 +219,6 @@ describe("OpenAi app-server event bridge", () => { await expect(bridge.failActiveTurns(context, failure)).resolves.toBe(true); await expect(completion).rejects.toThrow("app-server exited"); await expect(bridge.failActiveTurns(context, failure)).resolves.toBe(false); - await logger.destroy(); expect( events().filter((event) => @@ -257,7 +251,7 @@ describe("OpenAi app-server event bridge", () => { }); test("replays run start after delivery rejection before committing deduplication", async () => { - const { attempts, bridge, context, logger } = createHarness({ + const { attempts, bridge, context } = createHarness({ failReasonOnce: "driver.openai.turn.started", }); const started = { runId: DRIVER_TEST_IDS.runId, turnId: "turn-1" } as const; @@ -270,11 +264,10 @@ describe("OpenAi app-server event bridge", () => { ); expect(starts).toHaveLength(2); expect(starts[0]!.sourceEventId).toBe(starts[1]!.sourceEventId); - await logger.destroy(); }); test("final turn items do not duplicate already completed messages", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/completed", { item: { @@ -299,7 +292,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const assistantMessageId = readAssistantMessageId(events()); expect(events()).toMatchObject([ @@ -349,7 +341,7 @@ describe("OpenAi app-server event bridge", () => { }); test("rotates assistant message identity after each completed agent item", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const progressMessages = [ "进度 1:已完成读取。", @@ -433,7 +425,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const assistantMessages = events().flatMap((event) => { if (event.kind !== "message.delta") { @@ -461,7 +452,7 @@ describe("OpenAi app-server event bridge", () => { }); test("keeps interleaved assistant item identities isolated and ignores late replay", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/agentMessage/delta", { delta: "消息甲", @@ -502,7 +493,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const deltas = events().flatMap((event) => { if (event.kind !== "message.delta") { @@ -530,7 +520,7 @@ describe("OpenAi app-server event bridge", () => { }); test("command output streams as tool result content", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/started", { item: { @@ -552,7 +542,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-1", }); - await logger.destroy(); const assistantMessageId = readAssistantMessageId(events()); expect(events()).toMatchObject([ @@ -605,7 +594,7 @@ describe("OpenAi app-server event bridge", () => { }); test("turn plan updates map to the session plan custom event", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "turn/plan/updated", { explanation: null, @@ -637,7 +626,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-1", }); - await logger.destroy(); expect(events()).toEqual([ { diff --git a/tests/openai-app-server-event-bridge-terminal.test.ts b/tests/openai-app-server-event-bridge-terminal.test.ts index e7abd7e..96ec863 100644 --- a/tests/openai-app-server-event-bridge-terminal.test.ts +++ b/tests/openai-app-server-event-bridge-terminal.test.ts @@ -4,7 +4,7 @@ import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import type { AgentDriverPermissionPort, DriverPermissionRequest } from "../src/host-ports"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; @@ -61,11 +61,6 @@ function createPublisherHarness( const partialAttempts: DriverEventInput[][] = []; const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); let nextSeq = 1; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "openai-app-server-event-bridge-publisher-test", - sink: async () => {}, - }); const context: AgentDriverContext = createAgentDriverContext({ eventSink: { currentRunId: () => DRIVER_TEST_IDS.runId, @@ -103,7 +98,7 @@ function createPublisherHarness( }; }, }, - logger, + logger: createDisabledLogger(), payload: bootPayload, permission: { request: options.requestPermission ?? (async () => "allow_once"), @@ -119,7 +114,7 @@ function createPublisherHarness( requireThreadId: () => threadId, }); - return { bridge, context, delivered, logger, partialAttempts }; + return { bridge, context, delivered, partialAttempts }; } describe("OpenAi app-server event bridge", () => { @@ -172,7 +167,6 @@ describe("OpenAi app-server event bridge", () => { }), ]); harness.bridge.rejectTurn("turn-1", new Error("test complete")); - await harness.logger.destroy(); }); test("replays usage after delivery rejection before committing its baseline", async () => { @@ -210,7 +204,6 @@ describe("OpenAi app-server event bridge", () => { expect(harness.events().filter((event) => event.kind === "usage.updated")).toHaveLength(1); harness.bridge.rejectTurn("turn-1", new Error("test complete")); - await harness.logger.destroy(); }); test("attributes cumulative usage growth across turns without replaying the previous total", async () => { @@ -257,7 +250,6 @@ describe("OpenAi app-server event bridge", () => { expect.objectContaining({ size: 200_000, totalTokens: 30, used: 30 }), ]); harness.bridge.rejectTurn("turn-2", new Error("test complete")); - await harness.logger.destroy(); }); test.each([ @@ -267,7 +259,7 @@ describe("OpenAi app-server event bridge", () => { ] as const)( "%s turns never emit resume metadata after the terminal event", async (outcome, terminalKind, publishesResume) => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); void trackedTurn.catch(() => {}); @@ -299,7 +291,6 @@ describe("OpenAi app-server event bridge", () => { expect(resumeIndexes).toHaveLength(publishesResume ? 1 : 0); expect(resumeIndexes.every((index) => index < terminalIndex)).toBe(true); expect(allEvents.slice(terminalIndex + 1)).toEqual([]); - await logger.destroy(); }, ); @@ -376,11 +367,10 @@ describe("OpenAi app-server event bridge", () => { { kind: "diagnostic.reported", runId: DRIVER_TEST_IDS.runId }, { kind: "run.completed", runId: DRIVER_TEST_IDS.runId }, ]); - await harness.logger.destroy(); }); test("bounds a large turn diff before real CMA admission", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); void trackedTurn.catch(() => {}); const diff = "x".repeat(1_100_000); @@ -403,11 +393,10 @@ describe("OpenAi app-server event bridge", () => { }); expect(JSON.stringify(diagnostic)).not.toContain(diff.slice(0, 1_024)); bridge.rejectTurn("turn-1", new Error("test complete")); - await logger.destroy(); }); test("bounds non-durable official telemetry before real CMA admission", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const large = "x".repeat(1_100_000); const notifications = [ parseServerNotification({ @@ -496,13 +485,12 @@ describe("OpenAi app-server event bridge", () => { true, ); expect(JSON.stringify(delivered)).not.toContain(large); - await logger.destroy(); }); test("maps official approval identities and bounds terminal telemetry before CMA", async () => { const permission = Promise.withResolvers(); const nativeThreadId = `thread-${"i".repeat(300)}`; - const { bridge, context, delivered, logger } = createPublisherHarness({ + const { bridge, context, delivered } = createPublisherHarness({ requestPermission: async (input) => { permission.resolve(input); return "allow_once"; @@ -686,11 +674,10 @@ describe("OpenAi app-server event bridge", () => { expect(errors).toEqual([]); bridge.rejectTurn(nativeTurnId, new Error("test complete")); await handler.abortAll(new Error("test complete")); - await logger.destroy(); }); test("chunks large user-facing warnings and bounds world-writable samples for CMA", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const large = "x".repeat(1_100_000); const notifications = [ parseServerNotification({ @@ -736,11 +723,10 @@ describe("OpenAi app-server event bridge", () => { expect(delivered.every((event) => Buffer.byteLength(JSON.stringify(event)) < 1_048_576)).toBe( true, ); - await logger.destroy(); }); test("bounds an official failed-turn error before terminal CMA admission", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); const large = "x".repeat(1_100_000); const notification = parseServerNotification({ @@ -777,11 +763,10 @@ describe("OpenAi app-server event bridge", () => { }, }); expect(Buffer.byteLength(JSON.stringify(terminal))).toBeLessThan(1_048_576); - await logger.destroy(); }); test("removes OpenAI private citation markup from streamed and final assistant text", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const privateCitation = "\uE200cite\uE202turn7search12\uE202turn8view0\uE201"; await bridge.handleNotification(context, "item/agentMessage/delta", { @@ -825,7 +810,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const allEvents = events(); const streamedText = allEvents @@ -846,7 +830,7 @@ describe("OpenAi app-server event bridge", () => { }); test("flushes incomplete private markup when an item completes without a text snapshot", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/agentMessage/delta", { delta: "before\uE200cite\uE202turn7search12", @@ -862,7 +846,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-1", }); - await logger.destroy(); const streamedText = events() .filter((event) => event.kind === "message.delta") @@ -873,7 +856,7 @@ describe("OpenAi app-server event bridge", () => { }); test("does not fall back to progress when the final turn item is incomplete", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/completed", { item: { @@ -895,7 +878,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); @@ -908,7 +890,7 @@ describe("OpenAi app-server event bridge", () => { }); test("uses the last completed assistant snapshot when turn items are omitted", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/agentMessage/delta", { delta: "PROGRESS", @@ -947,7 +929,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); @@ -959,7 +940,7 @@ describe("OpenAi app-server event bridge", () => { }); test("uses completed snapshots when terminal items are not loaded", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/completed", { item: { id: "message-final", text: "FINAL", type: "agentMessage" }, @@ -986,7 +967,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); @@ -998,7 +978,7 @@ describe("OpenAi app-server event bridge", () => { }); test("does not fall back when a full terminal item list has no assistant", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/completed", { item: { id: "message-progress", text: "PROGRESS", type: "agentMessage" }, @@ -1014,7 +994,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); @@ -1026,7 +1005,7 @@ describe("OpenAi app-server event bridge", () => { }); test("uses first-seen item order when older completions arrive late", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); for (const [itemId, delta] of [ ["message-progress", "PROGRESS"], @@ -1053,7 +1032,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turn: { id: "turn-1", status: "completed" }, }); - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); @@ -1065,7 +1043,7 @@ describe("OpenAi app-server event bridge", () => { }); test("publishes more than 31 open tool closures before the run terminal", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const completion = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); const toolCount = 40; @@ -1091,7 +1069,6 @@ describe("OpenAi app-server event bridge", () => { }, }); await expect(completion).resolves.toBeUndefined(); - await logger.destroy(); expect( delivered.filter( @@ -1112,7 +1089,7 @@ describe("OpenAi app-server event bridge", () => { }); test("publishes a final snapshot larger than the terminal byte limit by reference", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const completion = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); const finalText = "x".repeat(1_100_000); @@ -1135,7 +1112,6 @@ describe("OpenAi app-server event bridge", () => { }, }); await expect(completion).resolves.toBeUndefined(); - await logger.destroy(); expectRunFinalReferences(delivered, finalText); expect(delivered.at(-1)?.kind).toBe("run.completed"); @@ -1143,7 +1119,7 @@ describe("OpenAi app-server event bridge", () => { test("commits a chunked snapshot once after a partial transport retry", async () => { const sourcePrefix = "openai.item.completed:turn-1:message-partial:"; - const { bridge, context, delivered, logger, partialAttempts } = createPublisherHarness({ + const { bridge, context, delivered, partialAttempts } = createPublisherHarness({ partialSourcePrefix: sourcePrefix, }); const completion = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); @@ -1186,11 +1162,10 @@ describe("OpenAi app-server event bridge", () => { ).toHaveLength(1); expectRunFinalReferences(delivered, finalText); expect(delivered.at(-1)?.kind).toBe("run.completed"); - await logger.destroy(); }); test("publishes a multi-byte final snapshot through CMA-safe lossless chunks", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const completion = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); const finalText = "界".repeat(400_000); @@ -1224,11 +1199,10 @@ describe("OpenAi app-server event bridge", () => { memoryCitation: { source: "memory-1" }, phase: "final", }); - await logger.destroy(); }); test("publishes a large command result once without exceeding CMA admission", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const output = "x".repeat(525_000); await bridge.handleNotification(context, "item/completed", { @@ -1251,11 +1225,10 @@ describe("OpenAi app-server event bridge", () => { ); expect(readEventPayloadString(completion!, "rawOutput")).toBe(output); expect(completion?.payload).not.toHaveProperty("content"); - await logger.destroy(); }); test("publishes a large dynamic result once through structured output", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const text = "x".repeat(600_000); const notification = parseServerNotification({ method: "item/completed", @@ -1290,11 +1263,10 @@ describe("OpenAi app-server event bridge", () => { expect(completion?.payload).toMatchObject({ structuredOutput: { contentItems: [{ text, type: "inputText" }] }, }); - await logger.destroy(); }); test("fails closed when one dynamic structured result exceeds CMA admission", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); void trackedTurn.catch(() => {}); const notification = parseServerNotification({ @@ -1336,11 +1308,10 @@ describe("OpenAi app-server event bridge", () => { readEventPayloadString(event, "status") === "completed", ), ).toBeUndefined(); - await logger.destroy(); }); test("fails the active turn instead of claiming an oversized tool result completed", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); void trackedTurn.catch(() => {}); const output = "x".repeat(1_100_000); @@ -1386,11 +1357,10 @@ describe("OpenAi app-server event bridge", () => { expect(delivered.find((event) => event.kind === "run.failed")?.payload).toMatchObject({ error: { message: expect.stringContaining("UTF-8 bytes") }, }); - await logger.destroy(); }); test("budgets the first message chunk around large citation metadata", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); const text = "a".repeat(512 * 1_024); const notification = parseServerNotification({ @@ -1434,11 +1404,10 @@ describe("OpenAi app-server event bridge", () => { (event.kind === "message.added" || event.kind === "message.delta"), ).length, ).toBeGreaterThan(1); - await logger.destroy(); }); test("budgets snapshot chunks after adding a long provider item identity", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); const itemId = `message-${"i".repeat(100_000)}`; const text = "a".repeat(512 * 1_024); @@ -1476,11 +1445,10 @@ describe("OpenAi app-server event bridge", () => { }); await expect(trackedTurn).resolves.toBeUndefined(); expectRunFinalReferences(delivered, text); - await logger.destroy(); }); test("maps long provider reasoning identity before durable CMA delivery", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); const itemId = `reasoning-${"i".repeat(262_000)}`; const text = "x".repeat(512 * 1_024); @@ -1516,11 +1484,10 @@ describe("OpenAi app-server event bridge", () => { expect(reasoningEvents.some((event) => event.sourceEventId?.includes(itemId) === true)).toBe( false, ); - await logger.destroy(); }); test("maps long provider item and tool identities before durable CMA delivery", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); const contextItemId = `compact-${"i".repeat(525_000)}`; const toolItemId = `tool-${"i".repeat(525_000)}`; @@ -1596,11 +1563,10 @@ describe("OpenAi app-server event bridge", () => { event.sourceEventId?.includes(toolItemId) === true, ), ).toBe(false); - await logger.destroy(); }); test("fails closed before CMA when one official plan update exceeds durable capacity", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); void trackedTurn.catch(() => {}); const notification = parseServerNotification({ @@ -1626,7 +1592,6 @@ describe("OpenAi app-server event bridge", () => { await expect(trackedTurn).rejects.toThrow("durable event capacity"); expect(delivered.some((event) => event.kind === "plan.updated")).toBe(false); expect(delivered.at(-1)?.kind).toBe("run.failed"); - await logger.destroy(); }); test.each([ @@ -1663,7 +1628,7 @@ describe("OpenAi app-server event bridge", () => { }, ], ] as const)("fails %s start before poisoning CMA terminal delivery", async (_label, item) => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); void trackedTurn.catch(() => {}); const notification = parseServerNotification({ @@ -1685,11 +1650,10 @@ describe("OpenAi app-server event bridge", () => { expect(delivered.some((event) => event.kind === "item.started")).toBe(false); expect(delivered.some((event) => event.kind === "tool.call.updated")).toBe(false); expect(delivered.at(-1)?.kind).toBe("run.failed"); - await logger.destroy(); }); test("completes a large renamed file patch without duplicating its diff", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); void trackedTurn.catch(() => {}); const diff = "x".repeat(1_050_000); @@ -1757,11 +1721,10 @@ describe("OpenAi app-server event bridge", () => { await bridge.failActiveTurns(context, new Error("test complete")); await expect(trackedTurn).rejects.toThrow("test complete"); - await logger.destroy(); }); test("fails closed when citation metadata alone exceeds CMA snapshot capacity", async () => { - const { bridge, context, delivered, logger } = createPublisherHarness(); + const { bridge, context, delivered } = createPublisherHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); void trackedTurn.catch(() => {}); const notification = parseServerNotification({ @@ -1806,11 +1769,10 @@ describe("OpenAi app-server event bridge", () => { expect(delivered.find((event) => event.kind === "message.failed")).toBeDefined(); expect(delivered.find((event) => event.kind === "run.failed")).toBeDefined(); expect(JSON.stringify(delivered)).not.toContain("memory-thread"); - await logger.destroy(); }); test("completed turns app final items before run finish", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "turn/completed", { threadId: "thread-1", @@ -1826,7 +1788,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const assistantMessageId = readAssistantMessageId(events()); expect(events()).toMatchObject([ diff --git a/tests/openai-app-server-event-bridge-transcript.test.ts b/tests/openai-app-server-event-bridge-transcript.test.ts index 2538078..1a339f1 100644 --- a/tests/openai-app-server-event-bridge-transcript.test.ts +++ b/tests/openai-app-server-event-bridge-transcript.test.ts @@ -9,7 +9,7 @@ import { describe("OpenAi app-server event bridge", () => { test("closes only open item state when a mixed turn is cancelled", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "item/reasoning/summaryTextDelta", { @@ -64,11 +64,10 @@ describe("OpenAi app-server event bridge", () => { }); await bridge.cancelTurn(context, "turn-1", "test.cancel.retry"); expect(events()).toHaveLength(terminalEventCount); - await logger.destroy(); }); test("does not duplicate streamed reasoning in the completed snapshot", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/reasoning/summaryTextDelta", { delta: "First", @@ -101,7 +100,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-1", }); - await logger.destroy(); expect( events() @@ -113,7 +111,7 @@ describe("OpenAi app-server event bridge", () => { }); test("releases translation state between turns and ignores late terminal replay", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/plan/delta", { delta: "old plan", @@ -170,7 +168,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-2", }); - await logger.destroy(); const planEvents = events().filter((event) => event.kind === "plan.updated"); expect(planEvents.at(-1)).toMatchObject({ @@ -187,7 +184,7 @@ describe("OpenAi app-server event bridge", () => { }); test("turn completion can arrive before the turn response is tracked", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "turn/completed", { threadId: "thread-1", @@ -198,7 +195,6 @@ describe("OpenAi app-server event bridge", () => { }); await expect(bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId)).resolves.toBeUndefined(); - await logger.destroy(); for (const event of events()) { expect(event.runId).toBeUndefined(); @@ -227,7 +223,7 @@ describe("OpenAi app-server event bridge", () => { }); test("resume metadata failure does not reject a completed turn", async () => { - const { bridge, context, events, logger } = createHarness({ + const { bridge, context, events } = createHarness({ failNativeResumePublish: true, }); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); @@ -242,13 +238,12 @@ describe("OpenAi app-server event bridge", () => { }), ).resolves.toBeUndefined(); await expect(trackedTurn).resolves.toBeUndefined(); - await logger.destroy(); expect(events().some((event) => event.kind === "run.completed")).toBe(true); }); test("turn errors wait for the authoritative failed turn", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "error", { error: { @@ -280,7 +275,6 @@ describe("OpenAi app-server event bridge", () => { await expect(trackedTurn).rejects.toThrow( "Response stream disconnected.\nHTTP 502 from upstream.", ); - await logger.destroy(); const failedEvent = events().find((event) => event.kind === "run.failed"); expect(failedEvent?.runId).toBe(DRIVER_TEST_IDS.runId); @@ -308,7 +302,7 @@ describe("OpenAi app-server event bridge", () => { }); test("thread systemError waits for the authoritative failed turn", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); await bridge.handleNotification(context, "thread/status/changed", { @@ -329,7 +323,6 @@ describe("OpenAi app-server event bridge", () => { }); await expect(trackedTurn).rejects.toThrow("The model returned an empty response."); - await logger.destroy(); expect(events().filter((event) => event.kind === "run.failed")).toMatchObject([ { payload: { @@ -346,7 +339,7 @@ describe("OpenAi app-server event bridge", () => { }); test("completed agent messages backfill text when no deltas streamed", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/completed", { item: { @@ -357,7 +350,6 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", turnId: "turn-1", }); - await logger.destroy(); const assistantMessageId = readAssistantMessageId(events()); expect(events()).toMatchObject([ @@ -388,7 +380,7 @@ describe("OpenAi app-server event bridge", () => { }); test("uses item completion text as the authoritative final snapshot", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await bridge.handleNotification(context, "item/agentMessage/delta", { delta: "损坏的流式片段", @@ -418,7 +410,6 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); - await logger.destroy(); const runCompleted = events().find((event) => event.kind === "run.completed"); diff --git a/tests/openai-app-server-provider-fixtures.test.ts b/tests/openai-app-server-provider-fixtures.test.ts index 31f5bb4..0695c0a 100644 --- a/tests/openai-app-server-provider-fixtures.test.ts +++ b/tests/openai-app-server-provider-fixtures.test.ts @@ -1,24 +1,20 @@ import { describe, expect, test } from "bun:test"; -import { readFileSync } from "node:fs"; -import { createBufferedSinkLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; -import { isDriverId } from "../src/protocol/id"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; +import { isRecord } from "../src/runtimes/openai/app-server-json"; import { - CLIENT_REQUEST_RESULT_PARSERS, + CLIENT_RESULT_SCHEMAS, isServerRequestMethod, parseServerNotification, } from "../src/runtimes/openai/app-server-protocol"; import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; -import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; - -interface EventBatch { - readonly events: DriverEventInput[]; - readonly reason: string; -} +import { createOpenAiBridgeHarness as createHarness } from "./openai-app-server-event-bridge-fixture"; +import { + normalizeOpenAiProviderEvents as normalizeBridgeEvents, + readProviderFixture, +} from "./provider-fixture-test-helpers"; interface ProviderNotificationFixture { readonly method: string; @@ -48,221 +44,32 @@ const providerFixtureNames = [ "unknown-notification-ignored", ] as const; -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function readJsonFixture(path: string): unknown { - return JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")); -} - -function readTrackTurnFixture(value: unknown): TrackTurnFixture | undefined { - if (value === undefined) { - return undefined; - } - - if (!isRecord(value)) { - throw new Error("Provider fixture trackTurnAfterNotifications must be an object."); - } - - const expectation = value["expectation"]; - const turnId = value["turnId"]; - - if ((expectation !== "reject" && expectation !== "resolve") || typeof turnId !== "string") { - throw new Error("Provider fixture trackTurnAfterNotifications is malformed."); - } - - return { - expectation, - turnId, - }; -} - -function readProviderNotificationFixture(value: unknown): ProviderNotificationFixture { - if (!isRecord(value)) { - throw new Error("Provider fixture notification must be an object."); - } - - const method = value["method"]; - - if (typeof method !== "string") { - throw new Error("Provider fixture notification method must be a string."); - } - - return { - method, - params: value["params"], - }; -} - function readProviderFixtureCase(path: string): ProviderFixtureCase { - const fixture = readJsonFixture(path); - - if (!isRecord(fixture)) { - throw new Error("Provider fixture must be an object."); - } - - const notifications = fixture["notifications"]; - const expectedEvents = fixture["expectedEvents"]; - - if (!Array.isArray(notifications) || !Array.isArray(expectedEvents)) { - throw new Error("Provider fixture must include notifications and expectedEvents arrays."); - } - - return { - expectedEvents, - notifications: notifications.map(readProviderNotificationFixture), - trackTurnBeforeNotifications: readTrackTurnFixture(fixture["trackTurnBeforeNotifications"]), - trackTurnAfterNotifications: readTrackTurnFixture(fixture["trackTurnAfterNotifications"]), - }; -} - -function isIsoTimestamp(value: string): boolean { - return value.endsWith("Z") && !Number.isNaN(Date.parse(value)); -} - -function collectDriverIds(value: unknown, aliases: Map, fieldName?: string): void { - if (typeof value === "string") { - const candidates = fieldName === "sourceEventId" ? value.split(":") : [value]; - for (const candidate of candidates) { - if (isDriverId(candidate) && !aliases.has(candidate)) { - aliases.set( - candidate, - aliases.size === 0 ? "" : ``, - ); - } - } - return; - } - - if (Array.isArray(value)) { - for (const entry of value) { - collectDriverIds(entry, aliases); - } - return; - } - - if (isRecord(value)) { - for (const [key, entry] of Object.entries(value)) { - collectDriverIds(entry, aliases, key); - } - } -} - -function normalizeBridgeValue( - value: unknown, - aliases: ReadonlyMap, - fieldName?: string, -): unknown { - if (typeof value === "string") { - const alias = aliases.get(value); - if (alias !== undefined) { - return alias; - } - - if (fieldName !== undefined && fieldName.endsWith("At") && isIsoTimestamp(value)) { - return ""; - } + const fixture = readProviderFixture(path, { + arrays: ["expectedEvents", "notifications"], + }); - if (fieldName === "sourceEventId") { - return value - .split(":") - .map((part) => aliases.get(part) ?? part) - .join(":"); + for (const notification of fixture.notifications) { + if (!isRecord(notification) || typeof notification["method"] !== "string") { + throw new TypeError(`Provider fixture ${path} has a malformed notification.`); } - - return value; - } - - if (Array.isArray(value)) { - return value.map((entry) => normalizeBridgeValue(entry, aliases)); - } - - if (!isRecord(value)) { - return value; } - return Object.fromEntries( - Object.entries(value).map(([key, entry]) => [key, normalizeBridgeValue(entry, aliases, key)]), - ); -} - -function normalizeBridgeEvent( - event: DriverEventInput, - aliases: ReadonlyMap, -): Record { - const eventRecord = event as unknown as Record; - const normalized: Record = { - kind: event.kind, - payload: normalizeBridgeValue(event.payload, aliases), - }; - - for (const field of ["delivery", "native", "runId", "sourceEventId", "visibility"] as const) { - if (eventRecord[field] !== undefined) { - normalized[field] = normalizeBridgeValue(eventRecord[field], aliases, field); + for (const trackTurn of [ + fixture.trackTurnBeforeNotifications, + fixture.trackTurnAfterNotifications, + ]) { + if ( + trackTurn !== undefined && + (!isRecord(trackTurn) || + (trackTurn["expectation"] !== "reject" && trackTurn["expectation"] !== "resolve") || + typeof trackTurn["turnId"] !== "string") + ) { + throw new TypeError(`Provider fixture ${path} has malformed turn tracking.`); } } - return normalized; -} - -function normalizeBridgeEvents(events: readonly DriverEventInput[]): Record[] { - const aliases = new Map(); - for (const event of events) { - collectDriverIds(event, aliases); - } - return events.map((event) => normalizeBridgeEvent(event, aliases)); -} - -function createHarness(input: { sharedRootPath?: string } = {}) { - const batches: EventBatch[] = []; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "openai-app-server-provider-fixtures-test", - sink: async () => {}, - }); - const context: AgentDriverContext = createAgentDriverContext({ - eventSink: { - currentRunId: () => DRIVER_TEST_IDS.runId, - pushEvents: async () => ({ accepted: [] }), - }, - logger, - payload: { - ...bootPayload, - execution: { - ...bootPayload.execution, - session: { - ...bootPayload.execution.session, - sharedRootPath: input.sharedRootPath ?? bootPayload.execution.session.sharedRootPath, - }, - }, - }, - permission: { - request: async () => "allow_once", - }, - }); - const push = async (_context: AgentDriverContext, reason: string, events: DriverEventInput[]) => { - batches.push({ events, reason }); - }; - const bridge = new OpenAiAppServerEventBridge({ - push, - pushSession: push, - pushTerminal: async (pushContext, reason, closures, terminal) => { - for (const closure of closures) { - await push(pushContext, `${reason}.items`, [closure]); - } - await push(pushContext, reason, [terminal]); - }, - requireThreadId: () => "thread-1", - }); - - return { - batches, - bridge, - context, - events: () => batches.flatMap((batch) => batch.events), - logger, - }; + return fixture; } async function dispatchProviderNotification( @@ -577,7 +384,7 @@ describe("OpenAI app-server provider fixtures", () => { }); test("classifies unused MCP-stream and realtime timeline notifications explicitly", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const realtimeItem = { id: "realtime-item-1", realtimeSessionId: "realtime-1", @@ -611,7 +418,6 @@ describe("OpenAI app-server provider fixtures", () => { } expect(events()).toEqual([]); - await logger.destroy(); }); test.each([undefined, "unknown", "inProgress"])( @@ -663,7 +469,7 @@ describe("OpenAI app-server provider fixtures", () => { }; delete turn[missing]; - expect(() => CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ turn })).toThrow(missing); + expect(() => CLIENT_RESULT_SCHEMAS["turn/start"].parse({ turn })).toThrow(missing); }, ); @@ -678,7 +484,7 @@ describe("OpenAI app-server provider fixtures", () => { }; delete response[missing]; - expect(() => CLIENT_REQUEST_RESULT_PARSERS.initialize(response)).toThrow(missing); + expect(() => CLIENT_RESULT_SCHEMAS.initialize.parse(response)).toThrow(missing); }, ); @@ -690,7 +496,7 @@ describe("OpenAI app-server provider fixtures", () => { userAgent: "test-app-server/0.150.1", }; - expect(CLIENT_REQUEST_RESULT_PARSERS.initialize(response)).toEqual(response); + expect(CLIENT_RESULT_SCHEMAS.initialize.parse(response)).toEqual(response); }); test.each([ @@ -705,15 +511,15 @@ describe("OpenAI app-server provider fixtures", () => { const response: Record = { ...createThreadStartFixture() }; delete response[missing]; - expect(() => CLIENT_REQUEST_RESULT_PARSERS["thread/start"](response)).toThrow(missing); + expect(() => CLIENT_RESULT_SCHEMAS["thread/start"].parse(response)).toThrow(missing); }); test("preserves the complete thread/start response and rejects a sparse thread", () => { const response = createThreadStartFixture(); - expect(CLIENT_REQUEST_RESULT_PARSERS["thread/start"](response)).toEqual(response); + expect(CLIENT_RESULT_SCHEMAS["thread/start"].parse(response)).toEqual(response); expect(() => - CLIENT_REQUEST_RESULT_PARSERS["thread/start"]({ + CLIENT_RESULT_SCHEMAS["thread/start"].parse({ ...response, thread: { id: "thread-1" }, }), @@ -736,14 +542,14 @@ describe("OpenAI app-server provider fixtures", () => { delete response["reasoningEffort"]; delete response["serviceTier"]; - expect(CLIENT_REQUEST_RESULT_PARSERS[method](response)).toEqual(response); + expect(CLIENT_RESULT_SCHEMAS[method].parse(response)).toEqual(response); }, ); test("applies the official thread/resume pagination defaults", () => { const response = createThreadStartFixture(); - expect(CLIENT_REQUEST_RESULT_PARSERS["thread/resume"](response)).toEqual({ + expect(CLIENT_RESULT_SCHEMAS["thread/resume"].parse(response)).toEqual({ ...response, initialTurnsPage: null, itemsBackwardsCursor: null, @@ -763,7 +569,7 @@ describe("OpenAI app-server provider fixtures", () => { turnsBackwardsCursor: "turn-head", }; - expect(CLIENT_REQUEST_RESULT_PARSERS["thread/resume"](response)).toEqual(response); + expect(CLIENT_RESULT_SCHEMAS["thread/resume"].parse(response)).toEqual(response); }); test("preserves structured turn errors and rejects unknown thread items", () => { @@ -777,9 +583,9 @@ describe("OpenAI app-server provider fixtures", () => { status: "failed", }; - expect(CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ turn })).toEqual({ turn }); + expect(CLIENT_RESULT_SCHEMAS["turn/start"].parse({ turn })).toEqual({ turn }); expect(() => - CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ + CLIENT_RESULT_SCHEMAS["turn/start"].parse({ turn: { ...createTurnFixture(), items: [{ id: "item-1", type: "futureItem" }] }, }), ).toThrow("Invalid input"); @@ -792,7 +598,7 @@ describe("OpenAI app-server provider fixtures", () => { ["inProgress", { message: "unexpected" }], ] as const)("rejects turn/start status %s with contradictory error", (status, error) => { expect(() => - CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ + CLIENT_RESULT_SCHEMAS["turn/start"].parse({ turn: { error, id: "turn-1", items: [], status }, }), ).toThrow("turn.error must be present exactly when the turn failed"); @@ -800,7 +606,7 @@ describe("OpenAI app-server provider fixtures", () => { test("accepts an in-progress turn/start without an error", () => { expect( - CLIENT_REQUEST_RESULT_PARSERS["turn/start"]({ + CLIENT_RESULT_SCHEMAS["turn/start"].parse({ turn: { id: "turn-1", items: [], status: "inProgress" }, }), ).toMatchObject({ turn: { status: "inProgress" } }); @@ -823,7 +629,7 @@ describe("OpenAI app-server provider fixtures", () => { }); test("rejects a non-object thread/inject_items response", () => { - expect(() => CLIENT_REQUEST_RESULT_PARSERS["thread/inject_items"](null)).toThrow( + expect(() => CLIENT_RESULT_SCHEMAS["thread/inject_items"].parse(null)).toThrow( "expected object", ); }); @@ -873,7 +679,7 @@ describe("OpenAI app-server provider fixtures", () => { }); test("maps official collaboration items to complete tool lifecycles", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const cases = [ { agentStatus: "completed", @@ -980,8 +786,6 @@ describe("OpenAI app-server provider fixtures", () => { }, }); } - - await logger.destroy(); }); test.each([ @@ -990,7 +794,7 @@ describe("OpenAI app-server provider fixtures", () => { ["interruptAgent", "Interrupt agent"], ["listAgents", "List agents"], ] as const)("maps the new %s collaboration tool lifecycle", async (tool, title) => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const baseItem = { agentsStates: {}, id: `collab-${tool}`, @@ -1034,11 +838,10 @@ describe("OpenAI app-server provider fixtures", () => { payload: expect.objectContaining({ status: "completed" }), }), ); - await logger.destroy(); }); test("maps official sleep items to tool lifecycle", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const item = { durationMs: 2_000, id: "sleep-1", type: "sleep" } as const; for (const method of ["item/started", "item/completed"] as const) { @@ -1067,11 +870,10 @@ describe("OpenAI app-server provider fixtures", () => { updates.map((event) => (isRecord(event.payload) ? event.payload["status"] : null)), ).toEqual(["running", "completed"]); expect(updates.at(-1)).toMatchObject({ payload: { rawOutput: "Slept for 2000 ms." } }); - await logger.destroy(); }); test("preserves command, MCP, and dynamic tool inputs and structured results", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const items = [ { aggregatedOutput: "ok\n", @@ -1171,13 +973,12 @@ describe("OpenAI app-server provider fixtures", () => { success: true, }, }); - await logger.destroy(); }); test("fails closed when no durable image transport is available", async () => { const pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await expect( dispatchProviderNotification( @@ -1202,11 +1003,10 @@ describe("OpenAI app-server provider fixtures", () => { ), ).rejects.toThrow("without a supported durable image transport"); expect(JSON.stringify(events())).not.toContain("/untrusted/provider.png"); - await logger.destroy(); }); test("maps the remaining official ThreadItem variants without silent data loss", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const completedItems = [ { action: { queries: ["OpenAI app-server 0.147"], query: null, type: "search" }, @@ -1289,13 +1089,12 @@ describe("OpenAI app-server provider fixtures", () => { ); } expect(events()).toHaveLength(eventCount); - await logger.destroy(); }); test.each(["started", "interacted", "interrupted", "completed"] as const)( "publishes official %s sub-agent activity from completion-only delivery", async (activityKind) => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const item = { agentPath: "/root/worker", agentThreadId: "agent-1", @@ -1335,12 +1134,11 @@ describe("OpenAI app-server provider fixtures", () => { sourceEventId: `openai.item.completed:turn-1:activity-${activityKind}:0`, }, ]); - await logger.destroy(); }, ); test("publishes sub-agent activity from a terminal turn snapshot", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await dispatchProviderNotification( { @@ -1378,7 +1176,6 @@ describe("OpenAI app-server provider fixtures", () => { }, sourceEventId: "openai.item.completed:turn-snapshot:activity-replayed:0", }); - await logger.destroy(); }); test.each([ @@ -1391,7 +1188,7 @@ describe("OpenAI app-server provider fixtures", () => { ] as const)( "fails closed on a schema-valid nonterminal %s completion before mutating state", async (type, runningStatus) => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const id = `nonterminal-${type}`; await expect( @@ -1432,12 +1229,11 @@ describe("OpenAI app-server provider fixtures", () => { payload: expect.objectContaining({ status: "failed", toolCallId: id }), }), ); - await logger.destroy(); }, ); test("rejects completed images and diagnoses provider-declared image failure", async () => { - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); await expect( dispatchProviderNotification( @@ -1511,14 +1307,13 @@ describe("OpenAI app-server provider fixtures", () => { }), ); expect(JSON.stringify(events())).not.toContain("/untrusted/failed.png"); - await logger.destroy(); }); test.each(providerFixtureNames)("apps provider-native fixture %s", async (name) => { const fixture = readProviderFixtureCase( `./fixtures/providers/openai-app-server/cases/${name}.json`, ); - const { bridge, context, events, logger } = createHarness(); + const { bridge, context, events } = createHarness(); const trackedTurn = fixture.trackTurnBeforeNotifications; const trackedCompletion = @@ -1538,7 +1333,6 @@ describe("OpenAI app-server provider fixtures", () => { } } await assertTrackTurnFixture(bridge, fixture.trackTurnAfterNotifications); - await logger.destroy(); expect(normalizeBridgeEvents(events())).toEqual(fixture.expectedEvents); }); diff --git a/tests/openai-app-server-request-handler.test.ts b/tests/openai-app-server-request-handler.test.ts index 70b5b28..e4d7684 100644 --- a/tests/openai-app-server-request-handler.test.ts +++ b/tests/openai-app-server-request-handler.test.ts @@ -3,7 +3,7 @@ import { expect, test } from "bun:test"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { PermissionEventDeliveryError } from "../src/core/driver-permission-broker"; import type { DriverPermissionRequest } from "../src/host-ports"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import { OpenAiAppServerRequestHandler } from "../src/runtimes/openai/app-server-request-handler"; import { driverStartInput } from "./driver-boot-payload-fixture"; @@ -14,17 +14,12 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as const responses: Array<{ id: string | number; result: unknown }> = []; const rejections: Array<{ id: string | number; message: string }> = []; const errors: Error[] = []; - const logger = createBufferedSinkLogger({ - level: "error", - service: "openai-request-handler-test", - sink: async () => {}, - }); const context = createAgentDriverContext({ eventSink: { currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload: driverStartInput, permission: { request: async (input, signal) => { @@ -140,24 +135,18 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as expect(errors).toEqual([]); } finally { await handler.abortAll(new Error("test complete")); - await logger.destroy(); } }); test("OpenAI writeStdin approval uses terminal-input semantics", async () => { const permission = Promise.withResolvers(); const response = Promise.withResolvers(); - const logger = createBufferedSinkLogger({ - level: "error", - service: "openai-request-handler-test", - sink: async () => {}, - }); const context = createAgentDriverContext({ eventSink: { currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload: driverStartInput, permission: { request: async (input) => { @@ -186,7 +175,6 @@ test("OpenAI writeStdin approval uses terminal-input semantics", async () => { title: "Approve terminal input", toolCallId: "command-1", }); - await logger.destroy(); }); test("OpenAI server request cancellation propagates permission delivery failures", async () => { @@ -198,17 +186,12 @@ test("OpenAI server request cancellation propagates permission delivery failures new Error("event sink unavailable"), ); const handledErrors: Error[] = []; - const logger = createBufferedSinkLogger({ - level: "error", - service: "openai-request-handler-test", - sink: async () => {}, - }); const context = createAgentDriverContext({ eventSink: { currentRunId: () => null, pushEvents: async () => ({ accepted: [] }), }, - logger, + logger: createDisabledLogger(), payload: driverStartInput, permission: { request: async (_input, signal) => { @@ -252,6 +235,5 @@ test("OpenAI server request cancellation propagates permission delivery failures } finally { deliveryGate.resolve(); await handler.abortAll(new Error("test complete")); - await logger.destroy(); } }); diff --git a/tests/openai-app-server-startup.test.ts b/tests/openai-app-server-startup.test.ts index 6c9980f..c8d7ff1 100644 --- a/tests/openai-app-server-startup.test.ts +++ b/tests/openai-app-server-startup.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentDriverPermissionPort } from "../src/host-ports"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import type { DriverPermissionPolicy } from "../src/protocol/boot"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; @@ -451,11 +451,7 @@ process.stdin.on("data", (chunk) => { let toolCompletionHolds = 0; const turnTimingEntered = Promise.withResolvers(); const turnTimingGate = Promise.withResolvers(); - const logger = createBufferedSinkLogger({ - level: "debug", - service: "openai-app-server-startup-test", - sink: async () => {}, - }); + const logger = createDisabledLogger(); const context = createAgentDriverContext({ eventSink: { commandUpdate: async () => {}, @@ -675,12 +671,11 @@ describe("OpenAI app-server startup", () => { if (!stopped) { await harness.backend.stop(harness.context, "test complete", new AbortController().signal); } - await harness.logger.destroy(); } }); test("injects platform transcript without publishing an unmaterialized thread", async () => { - const { backend, context, events, logger, requestLog } = await createHarness( + const { backend, context, events, requestLog } = await createHarness( "no rollout found for thread id stale-thread", ); @@ -707,11 +702,10 @@ describe("OpenAI app-server startup", () => { threadId: "fresh-thread", }); await backend.stop(context, "test complete", new AbortController().signal); - await logger.destroy(); }); test("stop cleans background terminals even when the thread has no active turn", async () => { - const { backend, context, logger, requestLog } = await createCancellationHarness({}); + const { backend, context, requestLog } = await createCancellationHarness({}); try { await backend.start(context, new AbortController().signal); @@ -724,7 +718,6 @@ describe("OpenAI app-server startup", () => { expect(methods).toContain("thread/backgroundTerminals/clean"); } finally { await backend.stop(context, "test complete", new AbortController().signal).catch(() => {}); - await logger.destroy(); } }); @@ -762,7 +755,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }, ); @@ -827,12 +819,11 @@ describe("OpenAI app-server startup", () => { ).resolves.toBeUndefined(); } finally { await harness.backend.stop(harness.context, "test complete", new AbortController().signal); - await harness.logger.destroy(); } }); test("does not replace the thread for other resume failures", async () => { - const { backend, context, events, logger } = await createHarness("Unauthorized"); + const { backend, context, events } = await createHarness("Unauthorized"); try { await expect(backend.start(context, new AbortController().signal)).rejects.toThrow( @@ -841,7 +832,6 @@ describe("OpenAI app-server startup", () => { expect(events).toEqual([]); } finally { await backend.stop(context, "test complete", new AbortController().signal); - await logger.destroy(); } }); @@ -858,7 +848,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }); @@ -876,7 +865,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }); @@ -910,7 +898,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }, 10_000); @@ -959,7 +946,6 @@ describe("OpenAI app-server startup", () => { new AbortController().signal, ); } - await harness.logger.destroy(); } }, ); @@ -1058,7 +1044,6 @@ describe("OpenAI app-server startup", () => { } finally { await harness.releaseTurnStartResponse(); await harness.backend.stop(harness.context, "test complete", new AbortController().signal); - await harness.logger.destroy(); } }, ); @@ -1115,7 +1100,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }); @@ -1208,7 +1192,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }, 10_000, @@ -1250,7 +1233,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }); @@ -1413,7 +1395,6 @@ describe("OpenAI app-server startup", () => { } finally { await harness.releaseTurnStartResponse(); await kernel.stop("test complete").catch(() => {}); - await harness.logger.destroy(); } }); @@ -1443,7 +1424,6 @@ describe("OpenAI app-server startup", () => { } finally { harness.releaseRunCancellation(); await harness.backend.stop(harness.context, "test complete", new AbortController().signal); - await harness.logger.destroy(); } }); @@ -1483,7 +1463,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }, 10_000); @@ -1525,7 +1504,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }, 10_000, @@ -1579,7 +1557,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }, 10_000); @@ -1629,7 +1606,6 @@ describe("OpenAI app-server startup", () => { ).toEqual(["run.cancel.requested", "run.cancelled"]); } finally { await harness.backend.stop(harness.context, "test complete", new AbortController().signal); - await harness.logger.destroy(); } }); @@ -1684,7 +1660,6 @@ describe("OpenAI app-server startup", () => { } finally { harness.releaseCancellationRequest(); await harness.backend.stop(harness.context, "test complete", new AbortController().signal); - await harness.logger.destroy(); } }); @@ -1759,7 +1734,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }); @@ -1824,7 +1798,6 @@ describe("OpenAI app-server startup", () => { } catch {} } stopSpy.mockRestore(); - await harness.logger.destroy(); } }, 10_000); @@ -1937,7 +1910,6 @@ describe("OpenAI app-server startup", () => { await kernel.stop("test complete").catch(() => {}); stopSpy?.mockRestore(); terminalSpy?.mockRestore(); - await harness.logger.destroy(); } }, 10_000, @@ -1969,7 +1941,6 @@ describe("OpenAI app-server startup", () => { .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); } - await harness.logger.destroy(); } }); @@ -2054,7 +2025,6 @@ describe("OpenAI app-server startup", () => { } catch {} } stopSpy.mockRestore(); - await harness.logger.destroy(); } }, 10_000); @@ -2106,7 +2076,6 @@ describe("OpenAI app-server startup", () => { } finally { permissionGate.resolve(); await harness.backend.stop(harness.context, "test complete", new AbortController().signal); - await harness.logger.destroy(); } }); @@ -2160,7 +2129,6 @@ describe("OpenAI app-server startup", () => { await harness.backend .stop(harness.context, "test complete", new AbortController().signal) .catch(() => {}); - await harness.logger.destroy(); } }, 10_000); }); diff --git a/tests/opencode-acp-contract.test.ts b/tests/opencode-acp-contract.test.ts index ae8e1f6..718be6c 100644 --- a/tests/opencode-acp-contract.test.ts +++ b/tests/opencode-acp-contract.test.ts @@ -21,7 +21,7 @@ import { } from "../src/runtimes/acp/acp-configuration"; import { limitAcpInput } from "../src/runtimes/acp/acp-input-limit"; import { setupAcpSession } from "../src/runtimes/acp/acp-session-setup"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger } from "../src/observability"; import { exposeNativeSkillAliases } from "../src/runtimes/skill-bootstrap"; import type { DriverStartInput } from "../src/protocol/start"; import { settlePromiseWithTimeout } from "../src/utils/async"; @@ -227,11 +227,7 @@ Check the diff.`, sharedRootPath: root, }, }; - const logger = createBufferedSinkLogger({ - level: "debug", - service: "opencode-acp-contract-test", - sink: async () => {}, - }); + const logger = createDisabledLogger(); try { await exposeNativeSkillAliases( @@ -263,7 +259,6 @@ Check the diff.`, false, ); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); diff --git a/tests/provider-fixture-test-helpers.ts b/tests/provider-fixture-test-helpers.ts new file mode 100644 index 0000000..b43d461 --- /dev/null +++ b/tests/provider-fixture-test-helpers.ts @@ -0,0 +1,159 @@ +import { readFileSync } from "node:fs"; + +import type { DriverEventInput } from "../src/protocol/events"; +import { isDriverId } from "../src/protocol/id"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function readProviderFixture( + path: string, + required: { readonly arrays: readonly string[]; readonly strings?: readonly string[] }, +): T { + const value: unknown = JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")); + + if ( + !isRecord(value) || + required.arrays.some((field) => !Array.isArray(value[field])) || + required.strings?.some((field) => typeof value[field] !== "string") === true + ) { + throw new TypeError(`Provider fixture ${path} is malformed.`); + } + + return value as T; +} + +type ProviderFixtureKind = "claude" | "openai"; + +function collectDriverIds( + value: unknown, + aliases: Map, + provider: ProviderFixtureKind, + fieldName?: string, +): void { + if (typeof value === "string") { + const candidates = + provider === "openai" && fieldName === "sourceEventId" ? value.split(":") : [value]; + + for (const candidate of candidates) { + if (isDriverId(candidate) && !aliases.has(candidate)) { + aliases.set( + candidate, + aliases.size === 0 ? "" : ``, + ); + } + } + return; + } + + if (Array.isArray(value)) { + for (const entry of value) { + collectDriverIds(entry, aliases, provider); + } + return; + } + + if (isRecord(value)) { + for (const [key, entry] of Object.entries(value)) { + collectDriverIds(entry, aliases, provider, key); + } + } +} + +function normalizeValue( + value: unknown, + aliases: ReadonlyMap, + provider: ProviderFixtureKind, + fieldName?: string, +): unknown { + if (typeof value === "string") { + if (provider === "claude") { + for (const [driverId, alias] of aliases) { + if (value === driverId || value.startsWith(`${driverId}:`)) { + return `${alias}${value.slice(driverId.length)}`; + } + } + } else { + const alias = aliases.get(value); + if (alias !== undefined) { + return alias; + } + } + + if ( + fieldName?.endsWith("At") === true && + value.endsWith("Z") && + !Number.isNaN(Date.parse(value)) + ) { + return ""; + } + + return provider === "openai" && fieldName === "sourceEventId" + ? value + .split(":") + .map((part) => aliases.get(part) ?? part) + .join(":") + : value; + } + + if (Array.isArray(value)) { + return value.map((entry) => normalizeValue(entry, aliases, provider)); + } + + if (!isRecord(value)) { + return value; + } + + const entries = Object.entries(value); + return Object.fromEntries( + provider === "claude" + ? entries.flatMap(([key, entry]) => + entry === undefined ? [] : [[key, normalizeValue(entry, aliases, provider, key)]], + ) + : entries.map(([key, entry]) => [key, normalizeValue(entry, aliases, provider, key)]), + ); +} + +function normalizeProviderEvents( + events: readonly DriverEventInput[], + provider: ProviderFixtureKind, +): Record[] { + const aliases = new Map(); + + for (const event of events) { + collectDriverIds(event, aliases, provider); + } + + return events.map((event) => { + if (provider === "claude") { + return normalizeValue(event, aliases, provider) as Record; + } + + const eventRecord = event as unknown as Record; + const normalized: Record = { + kind: event.kind, + payload: normalizeValue(event.payload, aliases, provider), + }; + + for (const field of ["delivery", "native", "runId", "sourceEventId", "visibility"] as const) { + if (eventRecord[field] !== undefined) { + normalized[field] = normalizeValue(eventRecord[field], aliases, provider, field); + } + } + + return normalized; + }); +} + +export function normalizeClaudeProviderEvents( + events: readonly DriverEventInput[], +): Record[] { + return normalizeProviderEvents(events, "claude"); +} + +export function normalizeOpenAiProviderEvents( + events: readonly DriverEventInput[], +): Record[] { + return normalizeProviderEvents(events, "openai"); +} diff --git a/tests/public-api-exports.snapshot.json b/tests/public-api-exports.snapshot.json index 67b64e0..78441bc 100644 --- a/tests/public-api-exports.snapshot.json +++ b/tests/public-api-exports.snapshot.json @@ -114,6 +114,7 @@ "CMA_DEFAULT_BETA_HEADER_NAME", "CMA_DEFAULT_BETA_HEADER_VALUE", "CmaInvalidEventError", + "CmaSdkClient", "CmaSdkError", "CmaSessionTerminatedError", "CmaStoreConflictError", @@ -128,7 +129,6 @@ "createAgentDriverProviderCapabilities", "createCmaHttpHandler", "createCmaMemoryStore", - "createCmaSdkClient", "createDriverDiagnosticEvent", "createDriverId", "getExpectedDriverNativeRuntimeRefKind", @@ -224,7 +224,7 @@ "CmaSdkStreamOptions", "CmaSessionEventDispatchRecord" ], - "values": ["CmaSdkError", "createCmaSdkClient"] + "values": ["CmaSdkClient", "CmaSdkError"] }, "./contract": { "types": [ diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index e790b6e..701f8a8 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { DRIVER_PROTOCOL_VERSION as DRIVER_PROTOCOL_VERSION_FROM_BOOT_SUBPATH } from "../src/boot"; import { createCmaHttpHandler as createCmaHttpHandlerFromSubpath } from "../src/cma-http"; -import { createCmaSdkClient as createCmaSdkClientFromSubpath } from "../src/cma-sdk"; +import { CmaSdkClient as CmaSdkClientFromSubpath } from "../src/cma-sdk"; import { PROTOCOL_VERSION as PROTOCOL_VERSION_FROM_CONTRACT, protocolVersionSchema as protocolVersionSchemaFromContract, @@ -13,6 +13,7 @@ import { AGENT_DRIVER_PROVIDER_REGISTRY, AgentDriverKernelCore, CMA_DEFAULT_BETA_HEADER_VALUE, + CmaSdkClient, CmaSdkError, SUPPORTED_DRIVER_RUNTIMES, createAgentDriverContext, @@ -20,7 +21,6 @@ import { createAgentDriverProviderCapabilities, createCmaHttpHandler, createCmaMemoryStore, - createCmaSdkClient, parseDriverNativeRuntimeRef, pushDriverDiagnosticEvent, projectCmaInboundToDriverCommand, @@ -43,7 +43,7 @@ describe("public API", () => { expect(createAgentDriverProviderCapabilities).toBeFunction(); expect(createCmaHttpHandler).toBeFunction(); expect(createCmaMemoryStore).toBeFunction(); - expect(createCmaSdkClient).toBeFunction(); + expect(CmaSdkClient).toBeFunction(); expect(CmaSdkError).toBeFunction(); expect(CMA_DEFAULT_BETA_HEADER_VALUE).toBe("managed-agents-2026-04-01"); expect(projectCmaInboundToDriverCommand).toBeFunction(); @@ -66,7 +66,7 @@ describe("public API", () => { expect(protocolVersionSchemaFromContract.parse(2)).toBe(2); expect(sessionSnapshotSchemaFromContract.parse).toBeFunction(); expect(createCmaHttpHandlerFromSubpath).toBe(createCmaHttpHandler); - expect(createCmaSdkClientFromSubpath).toBe(createCmaSdkClient); + expect(CmaSdkClientFromSubpath).toBe(CmaSdkClient); expect(parseDriverEventEnvelopeFromSubpath).toBeFunction(); expect(heartbeatReason).toBe("ping"); expect(parseDriverHeartbeatInputFromOrpcSubpath({ at: "now", pid: 1, reason: "ping" })).toEqual( diff --git a/tests/skill-materialization.test.ts b/tests/skill-materialization.test.ts index 5abb31a..857d868 100644 --- a/tests/skill-materialization.test.ts +++ b/tests/skill-materialization.test.ts @@ -20,7 +20,7 @@ import { dirname, join } from "node:path"; import { zipSync as createZipArchive } from "fflate"; import type { AgentDriverMaterializedSkill } from "../src/host-ports"; -import { createBufferedSinkLogger } from "../src/observability"; +import { createDisabledLogger as createTestLogger } from "../src/observability"; import type { DriverResolvedSkill } from "../src/protocol/boot"; import type { DriverExecutionInput } from "../src/protocol/execution"; import { exposeNativeSkillAliases } from "../src/runtimes/skill-bootstrap"; @@ -99,14 +99,6 @@ function exposeAliases( return exposeNativeSkillAliases(execution, logger, skills, signal); } -function createTestLogger() { - return createBufferedSinkLogger({ - level: "debug", - service: "skill-materialization-test", - sink: async () => {}, - }); -} - function createMarkdownSkillEntries(markdown: string) { return { "SKILL.md": textEncoder.encode(markdown) }; } @@ -153,7 +145,6 @@ Check the diff.`), "Check the diff.", ); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -179,7 +170,6 @@ Check the diff.`), "outside the allowed root", ); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -205,7 +195,6 @@ Check the diff.`), await expect(readFile(join(first.mountPath, "KEEP"), "utf8")).resolves.toBe("untouched"); await expect(readdir(join(root, ".mosoo"))).resolves.toEqual(["skill"]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -227,7 +216,6 @@ Check the diff.`), ); await expect(readdir(root)).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -248,7 +236,6 @@ Check the diff.`), await expect(materialize(execution, logger)).rejects.toThrow("does not match resolved skill"); await expect(readdir(root)).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -272,7 +259,6 @@ Check the diff.`), await expect(readFile(join(outsideMount, "KEEP"), "utf8")).resolves.toBe("outside"); await expect(readlink(join(root, ".mosoo", "skill"))).resolves.toBe(outsideRoot); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(outsideRoot, { force: true, recursive: true }); } @@ -295,7 +281,6 @@ Check the diff.`), await expect(readFile(join(outsideRoot, "KEEP"), "utf8")).resolves.toBe("outside"); await expect(readlink(skill.mountPath)).resolves.toBe(outsideRoot); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(outsideRoot, { force: true, recursive: true }); } @@ -316,7 +301,6 @@ Check the diff.`), await expect(readFile(join(outsideRoot, "KEEP"), "utf8")).resolves.toBe("outside"); await expect(readlink(join(root, ".mosoo", "skill"))).resolves.toBe(outsideRoot); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(outsideRoot, { force: true, recursive: true }); } @@ -335,7 +319,6 @@ Check the diff.`), ); await expect(readFile(mountRoot, "utf8")).resolves.toBe("user-controlled"); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -356,7 +339,6 @@ Check the diff.`), await expect(readFile(join(outsideRoot, "KEEP"), "utf8")).resolves.toBe("outside"); await expect(readlink(join(root, ".mosoo", "skill"))).resolves.toBe(outsideRoot); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(outsideRoot, { force: true, recursive: true }); } @@ -404,7 +386,6 @@ Check the diff.`), } finally { globalThis.fetch = nativeFetch; releaseResponse.resolve(); - await logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(outsideRoot, { force: true, recursive: true }); } @@ -466,7 +447,6 @@ Check the diff.`), await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).rejects.toThrow(); } finally { handlePrototype.sync = nativeSync; - await logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(outsideRoot, { force: true, recursive: true }); } @@ -493,7 +473,6 @@ Check the diff.`), "previous contents", ); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -522,7 +501,6 @@ Check the diff.`), readFile(join(skill.mountPath, ".mosoo-skill-cache.json"), "utf8"), ).rejects.toThrow(); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -546,7 +524,6 @@ Check the diff.`), ); await expect(readFile(join(outsideRoot, "KEEP"), "utf8")).resolves.toBe("outside"); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); await rm(outsideRoot, { force: true, recursive: true }); } @@ -567,7 +544,6 @@ Check the diff.`), await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).rejects.toThrow(); await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -582,7 +558,6 @@ Check the diff.`), await expect(exposeAliases(execution, logger, [])).resolves.toEqual([]); await expect(readdir(root)).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -624,7 +599,6 @@ Check the diff.`), } finally { releaseResponse.resolve(); globalThis.fetch = nativeFetch; - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -662,7 +636,6 @@ Check the diff.`), await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); } finally { handlePrototype.sync = nativeSync; - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -681,7 +654,6 @@ Check the diff.`), await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); await expect(readdir(transactionRoot)).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -705,7 +677,6 @@ Check the diff.`), await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); await expect(readdir(transactionRoot)).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -738,7 +709,6 @@ Check the diff.`), expect(sharedRootSynced).toBe(true); } finally { handlePrototype.sync = nativeSync; - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -767,7 +737,6 @@ Check the diff.`), ); await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -794,7 +763,6 @@ Check the diff.`), ); await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -825,7 +793,6 @@ Check the diff.`), ); await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -853,7 +820,6 @@ Check the diff.`), ); await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -879,7 +845,6 @@ Check the diff.`), await expect(materialize(createExecution(root, []), logger)).resolves.toEqual([]); await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -904,7 +869,6 @@ Check the diff.`), ); await expect(readdir(join(root, ".mosoo", ".skill-transactions"))).resolves.toEqual([]); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -968,7 +932,6 @@ Check the diff.`), ); } finally { globalThis.fetch = nativeFetch; - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -1006,7 +969,6 @@ Check the diff.`), ); } finally { globalThis.fetch = nativeFetch; - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -1039,7 +1001,6 @@ Check the diff.`), await expect(readFile(join(aliasPath, "SKILL.md"), "utf8")).rejects.toThrow(); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -1063,7 +1024,6 @@ Check the diff.`), await expect(exposeAliases(execution, logger, [skill])).resolves.toEqual([]); await expect(readdir(join(root, ".agents", "skills"))).rejects.toThrow(); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -1098,7 +1058,6 @@ Check the diff.`), ); await expect(readlink(join(root, ".agents", "skills", "review"))).rejects.toThrow(); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -1134,7 +1093,6 @@ Check the diff.`), await expect(readlink(aliasPath)).rejects.toThrow(); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); @@ -1167,7 +1125,6 @@ Check the diff.`), ).rejects.toThrow('Native skill alias "review" collides'); await expect(readFile(join(aliasPath, "KEEP"), "utf8")).resolves.toBe("user-owned"); } finally { - await logger.destroy(); await rm(root, { force: true, recursive: true }); } }); diff --git a/tsconfig.types.json b/tsconfig.types.json deleted file mode 100644 index 57aa706..0000000 --- a/tsconfig.types.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, - "noEmit": false, - "outDir": "dist/types", - "rootDir": "src", - "types": ["node"] - }, - "include": ["src/**/*.ts"] -} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..4ff33c3 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + pack: { + dts: { emitDtsOnly: true }, + entry: ["src/*.ts", "src/contract/index.ts"], + fixedExtension: false, + outDir: "dist/types", + platform: "neutral", + }, +}); From f3ccdb2d6f30076d9cbaecbfca75a009f35634af Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Thu, 27 Aug 2026 17:53:11 +0000 Subject: [PATCH 04/20] fix(acp): observe process exit independently of stdio --- src/runtimes/acp/acp-agent-process.ts | 38 +++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/runtimes/acp/acp-agent-process.ts b/src/runtimes/acp/acp-agent-process.ts index b77de8b..4b8af86 100644 --- a/src/runtimes/acp/acp-agent-process.ts +++ b/src/runtimes/acp/acp-agent-process.ts @@ -25,7 +25,7 @@ export type AcpAgentProcess = ChildProcessByStdio; const ACP_AGENT_EXIT_TIMEOUT_MS = 1_500; const ACP_AGENT_FORCE_KILL_TIMEOUT_MS = 500; -const agentProcessCloseTasks = new WeakMap>(); +const agentProcessExitTasks = new WeakMap>(); const agentProcessSupervision = new WeakMap(); interface AcpAgentProcessSupervision { @@ -91,16 +91,17 @@ export async function startAcpAgentProcess( agentProcessSupervision.set(agentProcess, supervision); const onAbort = () => signalAcpAgentProcess(agentProcess, "SIGKILL"); signal.addEventListener("abort", onAbort, { once: true }); - agentProcessCloseTasks.set( - agentProcess, - new Promise((resolve) => - agentProcess.once("close", () => { - signalLinuxProcessMarker(supervision.marker, "SIGKILL"); - signal.removeEventListener("abort", onAbort); - resolve(); - }), - ), - ); + const exited = Promise.withResolvers(); + const onExit = () => { + agentProcess.off("exit", onExit); + agentProcess.off("close", onExit); + signalLinuxProcessMarker(supervision.marker, "SIGKILL"); + signal.removeEventListener("abort", onAbort); + exited.resolve(); + }; + agentProcess.once("exit", onExit); + agentProcess.once("close", onExit); + agentProcessExitTasks.set(agentProcess, exited.promise); agentProcess.stderr.setEncoding("utf8"); agentProcess.stderr.on("data", (chunk: string) => { @@ -237,7 +238,7 @@ function releaseAcpAgentProcessSupervision(agentProcess: AcpAgentProcess): void return; } releaseLinuxProcessMarker(supervision.marker); - agentProcessCloseTasks.delete(agentProcess); + agentProcessExitTasks.delete(agentProcess); agentProcessSupervision.delete(agentProcess); } @@ -321,13 +322,16 @@ async function waitForChildProcessExit( process: AcpAgentProcess, timeoutMs: number, ): Promise { - const closed = agentProcessCloseTasks.get(process); + if (process.exitCode !== null || process.signalCode !== null) { + return true; + } + const exited = agentProcessExitTasks.get(process); - if (closed === undefined) { - return process.exitCode !== null || process.signalCode !== null; + if (exited === undefined) { + return false; } - const result = await settlePromiseWithTimeout(closed, { + const result = await settlePromiseWithTimeout(exited, { label: "ACP agent process exit", timeoutMs, }); @@ -337,7 +341,7 @@ async function waitForChildProcessExit( } if (result.status === "timed_out") { - return false; + return process.exitCode !== null || process.signalCode !== null; } throw result.error; From cf6cf1e5ce6c0c9ccc19727138e84210029cc15e Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Fri, 28 Aug 2026 20:59:01 +0000 Subject: [PATCH 05/20] refactor(runtime): align upstream protocol boundaries --- bench/ttft-bench.ts | 60 +- scripts/prune-openai-generated-types.mjs | 100 ++ src/bin/driver-process.ts | 7 - src/contract/common.ts | 5 +- src/core/agent-driver-backend.ts | 5 - src/host-ports/index.ts | 6 - src/index.ts | 2 - .../runtime/driver-instance-socket.ts | 205 ++-- src/protocol/boot/host-snapshot.ts | 159 ++- src/protocol/boot/index.ts | 726 +++++------- src/protocol/boot/readers.ts | 120 -- src/protocol/events/runtime-event-types.ts | 1 + src/protocol/events/runtime-events.ts | 62 + src/protocol/execution.ts | 3 + src/protocol/host-integration.ts | 19 - src/protocol/id/index.ts | 4 +- src/protocol/json.ts | 36 +- src/protocol/orpc/index.ts | 1004 +++++++---------- src/protocol/runtime.ts | 81 +- src/runtimes/acp/acp-driver-backend.ts | 20 - src/runtimes/acp/acp-session-setup.ts | 4 +- src/runtimes/acp/acp-turn-controller.ts | 4 +- .../claude/agent-sdk-message-translator.ts | 34 +- src/runtimes/claude/agent-sdk-task-events.ts | 216 ++-- .../openai/app-server-agent-task-events.ts | 160 +++ .../openai/app-server-driver-backend.ts | 70 +- .../openai/app-server-event-bridge.ts | 8 +- src/runtimes/openai/app-server-item-events.ts | 54 +- .../openai/app-server-protocol-types.ts | 69 +- src/runtimes/openai/auth-state.ts | 75 +- .../openai/generated-json-schema/README.md | 1 + src/runtimes/openai/generated/AgentPath.ts | 5 - .../generated/ApplyPatchApprovalParams.ts | 24 - .../generated/ApplyPatchApprovalResponse.ts | 6 - src/runtimes/openai/generated/AuthMode.ts | 16 - .../generated/AutoCompactTokenLimitScope.ts | 9 - .../openai/generated/ClientNotification.ts | 5 - .../openai/generated/ClientRequest.ts | 392 ------- .../generated/CodexResponseHandoffMode.ts | 5 - .../openai/generated/ConversationGitInfo.ts | 9 - .../openai/generated/ConversationSummary.ts | 19 - .../openai/generated/ConversationTextRole.ts | 5 - .../generated/ExecCommandApprovalParams.ts | 22 - .../generated/ExecCommandApprovalResponse.ts | 6 - .../openai/generated/ExecPolicyAmendment.ts | 12 - src/runtimes/openai/generated/FileChange.ts | 8 - .../openai/generated/ForcedLoginMethod.ts | 5 - .../generated/FuzzyFileSearchMatchType.ts | 5 - .../openai/generated/FuzzyFileSearchParams.ts | 9 - .../generated/FuzzyFileSearchResponse.ts | 6 - .../openai/generated/FuzzyFileSearchResult.ts | 16 - ...yFileSearchSessionCompletedNotification.ts | 5 - .../FuzzyFileSearchSessionStartParams.ts | 5 - .../FuzzyFileSearchSessionStartResponse.ts | 5 - .../FuzzyFileSearchSessionStopParams.ts | 5 - .../FuzzyFileSearchSessionStopResponse.ts | 5 - .../FuzzyFileSearchSessionUpdateParams.ts | 5 - .../FuzzyFileSearchSessionUpdateResponse.ts | 5 - ...zzyFileSearchSessionUpdatedNotification.ts | 10 - .../openai/generated/GetAuthStatusParams.ts | 5 - .../openai/generated/GetAuthStatusResponse.ts | 10 - .../generated/GetConversationSummaryParams.ts | 6 - .../GetConversationSummaryResponse.ts | 6 - .../openai/generated/GitDiffToRemoteParams.ts | 5 - .../generated/GitDiffToRemoteResponse.ts | 6 - src/runtimes/openai/generated/GitSha.ts | 5 - .../generated/ImageGenerationFailure.ts | 9 - .../openai/generated/ImageGenerationItem.ts | 15 - .../openai/generated/InitializeResponse.ts | 22 - .../openai/generated/InputModality.ts | 8 - .../openai/generated/InternalSessionSource.ts | 5 - .../openai/generated/McpServerInfo.ts | 16 - .../generated/NetworkPolicyAmendment.ts | 6 - .../generated/NetworkPolicyRuleAction.ts | 5 - .../openai/generated/ParsedCommand.ts | 20 - src/runtimes/openai/generated/PathUri.ts | 32 - src/runtimes/openai/generated/PlanType.ts | 22 - .../openai/generated/ProtocolMethods.ts | 96 ++ src/runtimes/openai/generated/README.md | 14 +- .../generated/RealtimeConversationVersion.ts | 5 - .../generated/RealtimeOutputModality.ts | 5 - .../openai/generated/RealtimeVoice.ts | 24 - .../openai/generated/RealtimeVoicesList.ts | 11 - src/runtimes/openai/generated/Resource.ts | 19 - .../openai/generated/ResourceContent.ts | 27 - .../openai/generated/ResourceTemplate.ts | 16 - .../openai/generated/ReviewDecision.ts | 18 - .../openai/generated/ServerNotification.ts | 193 ---- .../generated/ServerNotificationEnvelope.ts | 205 ---- .../openai/generated/ServerRequest.ts | 51 - .../openai/generated/SessionSource.ts | 15 - src/runtimes/openai/generated/SleepItem.ts | 8 - .../openai/generated/SubAgentSource.ts | 20 - src/runtimes/openai/generated/ThreadId.ts | 10 - .../openai/generated/ThreadMemoryMode.ts | 5 - src/runtimes/openai/generated/Tool.ts | 18 - src/runtimes/openai/generated/Verbosity.ts | 9 - .../openai/generated/WebSearchContextSize.ts | 5 - .../openai/generated/WebSearchItem.ts | 18 - .../openai/generated/WebSearchLocation.ts | 10 - .../openai/generated/WebSearchMode.ts | 5 - .../openai/generated/WebSearchToolConfig.ts | 11 - src/runtimes/openai/generated/index.ts | 100 -- src/runtimes/openai/generated/v2/Account.ts | 9 - .../v2/AccountLoginCompletedNotification.ts | 11 - .../AccountRateLimitsUpdatedNotification.ts | 13 - .../v2/AccountTokenUsageDailyBucket.ts | 5 - .../generated/v2/AccountTokenUsageSummary.ts | 11 - .../v2/AccountUpdatedNotification.ts | 7 - .../generated/v2/ActivePermissionProfile.ts | 16 - .../generated/v2/AddCreditsNudgeCreditType.ts | 5 - .../v2/AddCreditsNudgeEmailStatus.ts | 5 - .../v2/AdditionalFileSystemPermissions.ts | 18 - .../v2/AdditionalNetworkPermissions.ts | 5 - .../v2/AdditionalPermissionProfile.ts | 13 - .../generated/v2/AgentMessageDelivery.ts | 5 - .../v2/AgentMessageDeltaNotification.ts | 10 - .../generated/v2/AllowDenyRequirement.ts | 5 - .../openai/generated/v2/AnalyticsConfig.ts | 14 - .../openai/generated/v2/AppBranding.ts | 15 - src/runtimes/openai/generated/v2/AppInfo.ts | 34 - .../v2/AppListUpdatedNotification.ts | 9 - .../openai/generated/v2/AppMetadata.ts | 19 - src/runtimes/openai/generated/v2/AppReview.ts | 5 - .../openai/generated/v2/AppScreenshot.ts | 5 - .../openai/generated/v2/AppSummary.ts | 14 - .../openai/generated/v2/AppTemplateSummary.ts | 16 - .../v2/AppTemplateUnavailableReason.ts | 5 - .../openai/generated/v2/AppToolApproval.ts | 5 - .../openai/generated/v2/AppToolSummary.ts | 15 - .../openai/generated/v2/AppToolsConfig.ts | 8 - .../openai/generated/v2/AppsConfig.ts | 19 - .../openai/generated/v2/AppsDefaultConfig.ts | 13 - .../generated/v2/AppsInstalledParams.ts | 18 - .../generated/v2/AppsInstalledResponse.ts | 9 - .../openai/generated/v2/AppsListParams.ts | 25 - .../openai/generated/v2/AppsListResponse.ts | 16 - .../openai/generated/v2/AppsReadParams.ts | 22 - .../openai/generated/v2/AppsReadResponse.ts | 9 - .../generated/v2/AttestationGenerateParams.ts | 5 - .../v2/AttestationGenerateResponse.ts | 10 - .../generated/v2/AutoReviewDecisionSource.ts | 8 - .../generated/v2/AutoReviewRequirements.ts | 8 - .../openai/generated/v2/AwsCredentialType.ts | 5 - .../openai/generated/v2/BedrockAwsProfile.ts | 5 - .../generated/v2/BedrockDiscoverParams.ts | 5 - .../generated/v2/BedrockDiscoverResponse.ts | 10 - .../v2/BedrockEnvironmentCredential.ts | 6 - .../openai/generated/v2/BedrockSetupParams.ts | 7 - .../generated/v2/BedrockSetupResponse.ts | 5 - .../v2/BrowserUseAccessApprovalLifetime.ts | 5 - .../openai/generated/v2/BrowserUseConfig.ts | 10 - .../generated/v2/BrowserUseOriginPolicy.ts | 15 - .../v2/BrowserUseOriginPolicyConfig.ts | 11 - .../generated/v2/BrowserUseRequirements.ts | 12 - .../generated/v2/CancelLoginAccountParams.ts | 5 - .../v2/CancelLoginAccountResponse.ts | 6 - .../generated/v2/CancelLoginAccountStatus.ts | 5 - .../v2/ChatgptAuthTokensRefreshParams.ts | 18 - .../v2/ChatgptAuthTokensRefreshReason.ts | 5 - .../v2/ChatgptAuthTokensRefreshResponse.ts | 9 - .../v2/CliAuthCredentialsStoreMode.ts | 5 - .../openai/generated/v2/CodexErrorInfo.ts | 29 - .../openai/generated/v2/CollabAgentState.ts | 6 - .../openai/generated/v2/CollabAgentStatus.ts | 12 - .../openai/generated/v2/CollabAgentTool.ts | 14 - .../generated/v2/CollabAgentToolCallStatus.ts | 5 - .../v2/CollaborationModeListParams.ts | 8 - .../v2/CollaborationModeListResponse.ts | 9 - .../generated/v2/CollaborationModeMask.ts | 15 - .../openai/generated/v2/CommandAction.ts | 10 - .../v2/CommandExecOutputDeltaNotification.ts | 31 - .../generated/v2/CommandExecOutputStream.ts | 8 - .../openai/generated/v2/CommandExecParams.ts | 106 -- .../generated/v2/CommandExecResizeParams.ts | 19 - .../generated/v2/CommandExecResizeResponse.ts | 8 - .../generated/v2/CommandExecResponse.ts | 25 - .../generated/v2/CommandExecTerminalSize.ts | 17 - .../v2/CommandExecTerminateParams.ts | 14 - .../v2/CommandExecTerminateResponse.ts | 8 - .../generated/v2/CommandExecWriteParams.ts | 23 - .../generated/v2/CommandExecWriteResponse.ts | 8 - .../v2/CommandExecutionApprovalDecision.ts | 13 - .../v2/CommandExecutionApprovalKind.ts | 8 - ...CommandExecutionOutputDeltaNotification.ts | 10 - .../CommandExecutionRequestApprovalParams.ts | 76 -- ...CommandExecutionRequestApprovalResponse.ts | 8 - .../generated/v2/CommandExecutionSource.ts | 9 - .../generated/v2/CommandExecutionStatus.ts | 5 - .../openai/generated/v2/CommandMigration.ts | 5 - .../openai/generated/v2/ComputerUseConfig.ts | 12 - .../generated/v2/ComputerUseMacosConfig.ts | 8 - .../v2/ComputerUseMacosRequirements.ts | 8 - .../generated/v2/ComputerUseRequirements.ts | 14 - .../generated/v2/ComputerUseWindowsConfig.ts | 10 - .../v2/ComputerUseWindowsExeConfig.ts | 11 - .../v2/ComputerUseWindowsExeRequirement.ts | 11 - .../v2/ComputerUseWindowsRequirements.ts | 10 - src/runtimes/openai/generated/v2/Config.ts | 61 - .../generated/v2/ConfigBatchWriteParams.ts | 19 - .../openai/generated/v2/ConfigEdit.ts | 7 - .../openai/generated/v2/ConfigLayer.ts | 12 - .../generated/v2/ConfigLayerMetadata.ts | 6 - .../openai/generated/v2/ConfigLayerSource.ts | 52 - .../openai/generated/v2/ConfigReadParams.ts | 13 - .../openai/generated/v2/ConfigReadResponse.ts | 12 - .../openai/generated/v2/ConfigRequirements.ts | 52 - .../v2/ConfigRequirementsReadResponse.ts | 11 - .../generated/v2/ConfigValueWriteParams.ts | 16 - .../generated/v2/ConfigWarningNotification.ts | 23 - .../generated/v2/ConfigWriteResponse.ts | 16 - .../generated/v2/ConfiguredHookHandler.ts | 31 - .../v2/ConfiguredHookMatcherGroup.ts | 9 - .../openai/generated/v2/ConnectorMetadata.ts | 19 - ...nsumeAccountRateLimitResetCreditOutcome.ts | 9 - ...onsumeAccountRateLimitResetCreditParams.ts | 16 - ...sumeAccountRateLimitResetCreditResponse.ts | 8 - .../v2/ContextCompactedNotification.ts | 8 - .../openai/generated/v2/CreditsSnapshot.ts | 5 - .../generated/v2/CurrentTimeReadParams.ts | 5 - .../v2/DeprecationNoticeNotification.ts | 14 - .../v2/DesktopOnboardingEntrypoint.ts | 5 - .../v2/DynamicToolCallOutputContentItem.ts | 8 - .../generated/v2/DynamicToolCallParams.ts | 13 - .../generated/v2/DynamicToolCallResponse.ts | 9 - .../generated/v2/DynamicToolCallStatus.ts | 5 - .../generated/v2/EnvironmentAddParams.ts | 12 - .../generated/v2/EnvironmentAddResponse.ts | 5 - .../v2/EnvironmentConnectionNotification.ts | 5 - .../generated/v2/EnvironmentInfoParams.ts | 5 - .../generated/v2/EnvironmentInfoResponse.ts | 13 - .../generated/v2/EnvironmentShellInfo.ts | 14 - .../generated/v2/EnvironmentStatusKind.ts | 11 - .../generated/v2/EnvironmentStatusParams.ts | 13 - .../generated/v2/EnvironmentStatusResponse.ts | 18 - .../openai/generated/v2/ErrorNotification.ts | 11 - .../generated/v2/ExecPolicyAmendment.ts | 5 - .../generated/v2/ExperimentalFeature.ts | 38 - .../ExperimentalFeatureEnablementSetParams.ts | 13 - ...xperimentalFeatureEnablementSetResponse.ts | 10 - .../v2/ExperimentalFeatureListParams.ts | 20 - .../v2/ExperimentalFeatureListResponse.ts | 13 - .../generated/v2/ExperimentalFeatureStage.ts | 10 - .../v2/ExternalAgentConfigDetectParams.ts | 31 - .../v2/ExternalAgentConfigDetectResponse.ts | 10 - ...lAgentConfigImportCompletedNotification.ts | 9 - ...lAgentConfigImportHistoriesReadResponse.ts | 10 - .../v2/ExternalAgentConfigImportHistory.ts | 13 - ...nalAgentConfigImportHistoryRecordParams.ts | 15 - ...lAgentConfigImportHistoryRecordResponse.ts | 5 - ...tConfigImportHistoryRecordSuccessParams.ts | 15 - ...nfigImportHistoryRecordTypeResultParams.ts | 12 - ...xternalAgentConfigImportItemTypeFailure.ts | 14 - ...xternalAgentConfigImportItemTypeSuccess.ts | 15 - .../v2/ExternalAgentConfigImportParams.ts | 22 - ...alAgentConfigImportProgressNotification.ts | 9 - .../v2/ExternalAgentConfigImportResponse.ts | 5 - .../v2/ExternalAgentConfigImportTypeResult.ts | 12 - .../v2/ExternalAgentConfigMigrationItem.ts | 15 - .../ExternalAgentConfigMigrationItemType.ts | 15 - ...ExternalAgentDetectedConnectorCandidate.ts | 10 - .../ExternalAgentDetectedConnectorSource.ts | 5 - ...ExternalAgentImportedConnectorCandidate.ts | 10 - .../ExternalAgentImportedConnectorSource.ts | 5 - .../generated/v2/FeedbackRequirements.ts | 5 - .../generated/v2/FeedbackUploadParams.ts | 12 - .../generated/v2/FeedbackUploadResponse.ts | 5 - .../v2/FileChangeApprovalDecision.ts | 5 - .../v2/FileChangeOutputDeltaNotification.ts | 15 - .../v2/FileChangePatchUpdatedNotification.ts | 11 - .../v2/FileChangeRequestApprovalParams.ts | 22 - .../v2/FileChangeRequestApprovalResponse.ts | 6 - .../generated/v2/FileSystemAccessMode.ts | 5 - .../openai/generated/v2/FileSystemPath.ts | 10 - .../generated/v2/FileSystemSandboxEntry.ts | 7 - .../generated/v2/FileSystemSpecialPath.ts | 12 - .../openai/generated/v2/FileUpdateChange.ts | 6 - .../generated/v2/ForcedChatgptWorkspaceIds.ts | 8 - .../generated/v2/FsChangedNotification.ts | 18 - .../openai/generated/v2/FsCopyParams.ts | 22 - .../openai/generated/v2/FsCopyResponse.ts | 8 - .../generated/v2/FsCreateDirectoryParams.ts | 18 - .../generated/v2/FsCreateDirectoryResponse.ts | 8 - .../generated/v2/FsGetMetadataParams.ts | 14 - .../generated/v2/FsGetMetadataResponse.ts | 29 - .../generated/v2/FsReadDirectoryEntry.ts | 21 - .../generated/v2/FsReadDirectoryParams.ts | 14 - .../generated/v2/FsReadDirectoryResponse.ts | 14 - .../openai/generated/v2/FsReadFileParams.ts | 14 - .../openai/generated/v2/FsReadFileResponse.ts | 13 - .../openai/generated/v2/FsRemoveParams.ts | 22 - .../openai/generated/v2/FsRemoveResponse.ts | 8 - .../openai/generated/v2/FsUnwatchParams.ts | 13 - .../openai/generated/v2/FsUnwatchResponse.ts | 8 - .../openai/generated/v2/FsWatchParams.ts | 18 - .../openai/generated/v2/FsWatchResponse.ts | 14 - .../openai/generated/v2/FsWriteFileParams.ts | 18 - .../generated/v2/FsWriteFileResponse.ts | 8 - .../openai/generated/v2/GetAccountParams.ts | 14 - .../v2/GetAccountRateLimitsResponse.ts | 17 - .../openai/generated/v2/GetAccountResponse.ts | 6 - .../v2/GetAccountTokenUsageParams.ts | 10 - .../v2/GetAccountTokenUsageResponse.ts | 15 - .../v2/GetWorkspaceMessagesResponse.ts | 15 - src/runtimes/openai/generated/v2/GitInfo.ts | 5 - .../generated/v2/GrantedPermissionProfile.ts | 10 - .../generated/v2/GuardianApprovalReview.ts | 18 - .../v2/GuardianApprovalReviewAction.ts | 42 - .../v2/GuardianApprovalReviewStatus.ts | 13 - .../generated/v2/GuardianCommandSource.ts | 5 - .../openai/generated/v2/GuardianRiskLevel.ts | 8 - .../generated/v2/GuardianUserAuthorization.ts | 8 - .../v2/GuardianWarningNotification.ts | 14 - .../generated/v2/HookCompletedNotification.ts | 10 - .../openai/generated/v2/HookErrorInfo.ts | 5 - .../openai/generated/v2/HookEventName.ts | 17 - .../openai/generated/v2/HookExecutionMode.ts | 5 - .../openai/generated/v2/HookHandlerType.ts | 5 - .../openai/generated/v2/HookMetadata.ts | 33 - .../openai/generated/v2/HookMigration.ts | 5 - .../openai/generated/v2/HookOutputEntry.ts | 6 - .../generated/v2/HookOutputEntryKind.ts | 5 - .../openai/generated/v2/HookPromptFragment.ts | 5 - .../openai/generated/v2/HookRunStatus.ts | 5 - .../openai/generated/v2/HookRunSummary.ts | 28 - src/runtimes/openai/generated/v2/HookScope.ts | 5 - .../openai/generated/v2/HookSource.ts | 16 - .../generated/v2/HookStartedNotification.ts | 10 - .../openai/generated/v2/HookTrustStatus.ts | 5 - .../openai/generated/v2/HooksListEntry.ts | 12 - .../openai/generated/v2/HooksListParams.ts | 10 - .../openai/generated/v2/HooksListResponse.ts | 6 - .../generated/v2/InAppBrowserRequirements.ts | 5 - .../openai/generated/v2/InstalledApp.ts | 25 - .../generated/v2/ItemCompletedNotification.ts | 14 - ...dianApprovalReviewCompletedNotification.ts | 46 - ...ardianApprovalReviewStartedNotification.ts | 40 - .../generated/v2/ItemStartedNotification.ts | 14 - .../generated/v2/ListMcpServerStatusParams.ts | 21 - .../v2/ListMcpServerStatusResponse.ts | 13 - .../openai/generated/v2/LoginAccountParams.ts | 41 - .../generated/v2/LoginAccountResponse.ts | 28 - .../openai/generated/v2/LoginAppBrand.ts | 5 - .../generated/v2/LogoutAccountResponse.ts | 5 - .../generated/v2/ManagedHooksRequirements.ts | 21 - .../generated/v2/MarketplaceAddParams.ts | 9 - .../generated/v2/MarketplaceAddResponse.ts | 10 - .../generated/v2/MarketplaceInterface.ts | 5 - .../generated/v2/MarketplaceLoadErrorInfo.ts | 6 - .../generated/v2/MarketplaceRemoveParams.ts | 5 - .../generated/v2/MarketplaceRemoveResponse.ts | 9 - .../v2/MarketplaceUpgradeErrorInfo.ts | 5 - .../generated/v2/MarketplaceUpgradeParams.ts | 5 - .../v2/MarketplaceUpgradeResponse.ts | 11 - .../openai/generated/v2/McpAuthStatus.ts | 5 - .../generated/v2/McpElicitationArrayType.ts | 5 - .../v2/McpElicitationBooleanSchema.ts | 11 - .../generated/v2/McpElicitationBooleanType.ts | 5 - .../generated/v2/McpElicitationConstOption.ts | 5 - .../generated/v2/McpElicitationEnumSchema.ts | 11 - .../McpElicitationLegacyTitledEnumSchema.ts | 13 - .../v2/McpElicitationMultiSelectEnumSchema.ts | 9 - .../v2/McpElicitationNumberSchema.ts | 13 - .../generated/v2/McpElicitationNumberType.ts | 5 - .../generated/v2/McpElicitationObjectType.ts | 5 - .../v2/McpElicitationPrimitiveSchema.ts | 13 - .../generated/v2/McpElicitationSchema.ts | 18 - .../McpElicitationSingleSelectEnumSchema.ts | 9 - .../v2/McpElicitationStringFormat.ts | 5 - .../v2/McpElicitationStringSchema.ts | 15 - .../generated/v2/McpElicitationStringType.ts | 5 - .../v2/McpElicitationTitledEnumItems.ts | 6 - ...pElicitationTitledMultiSelectEnumSchema.ts | 15 - ...ElicitationTitledSingleSelectEnumSchema.ts | 13 - .../v2/McpElicitationUntitledEnumItems.ts | 9 - ...licitationUntitledMultiSelectEnumSchema.ts | 15 - ...icitationUntitledSingleSelectEnumSchema.ts | 12 - .../generated/v2/McpResourceReadParams.ts | 14 - .../generated/v2/McpResourceReadResponse.ts | 12 - .../generated/v2/McpServerConnectionStatus.ts | 12 - .../v2/McpServerElicitationAction.ts | 5 - .../v2/McpServerElicitationRequestParams.ts | 28 - .../v2/McpServerElicitationRequestResponse.ts | 19 - .../v2/McpServerEventNotification.ts | 6 - .../v2/McpServerEventStreamNotification.ts | 9 - .../v2/McpServerEventStreamStartParams.ts | 13 - .../v2/McpServerEventStreamStartResponse.ts | 5 - .../v2/McpServerEventStreamStopParams.ts | 5 - .../v2/McpServerEventStreamStopResponse.ts | 5 - .../openai/generated/v2/McpServerMigration.ts | 5 - .../v2/McpServerOauthClientRegistration.ts | 5 - ...cpServerOauthLoginCompletedNotification.ts | 10 - .../generated/v2/McpServerOauthLoginParams.ts | 15 - .../v2/McpServerOauthLoginResponse.ts | 5 - .../generated/v2/McpServerRefreshResponse.ts | 5 - .../v2/McpServerStartupFailureReason.ts | 5 - .../generated/v2/McpServerStartupState.ts | 5 - .../openai/generated/v2/McpServerStatus.ts | 23 - .../generated/v2/McpServerStatusDetail.ts | 5 - .../v2/McpServerStatusUpdatedNotification.ts | 13 - .../generated/v2/McpServerToolCallParams.ts | 12 - .../generated/v2/McpServerToolCallResponse.ts | 11 - .../generated/v2/McpToolCallAppContext.ts | 11 - .../openai/generated/v2/McpToolCallError.ts | 5 - .../v2/McpToolCallProgressNotification.ts | 10 - .../openai/generated/v2/McpToolCallResult.ts | 10 - .../openai/generated/v2/McpToolCallStatus.ts | 5 - .../openai/generated/v2/MemoryCitation.ts | 6 - .../generated/v2/MemoryCitationEntry.ts | 10 - .../generated/v2/MemoryResetResponse.ts | 5 - .../openai/generated/v2/MergeStrategy.ts | 5 - .../openai/generated/v2/MigrationDetails.ts | 21 - .../v2/MockExperimentalMethodParams.ts | 10 - .../v2/MockExperimentalMethodResponse.ts | 10 - src/runtimes/openai/generated/v2/Model.ts | 40 - .../generated/v2/ModelAvailabilityNux.ts | 5 - .../openai/generated/v2/ModelListParams.ts | 18 - .../openai/generated/v2/ModelListResponse.ts | 13 - .../v2/ModelProviderCapabilitiesReadParams.ts | 5 - .../ModelProviderCapabilitiesReadResponse.ts | 9 - .../openai/generated/v2/ModelRerouteReason.ts | 5 - .../generated/v2/ModelReroutedNotification.ts | 12 - ...ModelSafetyBufferingUpdatedNotification.ts | 13 - .../openai/generated/v2/ModelServiceTier.ts | 5 - .../openai/generated/v2/ModelUpgradeInfo.ts | 14 - .../openai/generated/v2/ModelVerification.ts | 5 - .../v2/ModelVerificationNotification.ts | 10 - .../openai/generated/v2/ModelsRequirements.ts | 6 - .../openai/generated/v2/MultiAgentVersion.ts | 8 - .../generated/v2/NetworkApprovalContext.ts | 6 - .../generated/v2/NetworkApprovalProtocol.ts | 5 - .../generated/v2/NetworkDomainPermission.ts | 5 - .../generated/v2/NetworkPolicyAmendment.ts | 6 - .../generated/v2/NetworkPolicyRuleAction.ts | 5 - .../generated/v2/NetworkRequirements.ts | 40 - .../v2/NetworkUnixSocketPermission.ts | 5 - .../generated/v2/NewThreadModelDefaults.ts | 10 - .../generated/v2/NonSteerableTurnKind.ts | 5 - .../openai/generated/v2/OverriddenMetadata.ts | 11 - .../openai/generated/v2/PatchApplyStatus.ts | 5 - .../openai/generated/v2/PatchChangeKind.ts | 8 - .../generated/v2/PermissionGrantScope.ts | 5 - .../v2/PermissionProfileListParams.ts | 18 - .../v2/PermissionProfileListResponse.ts | 13 - .../generated/v2/PermissionProfileSummary.ts | 18 - .../v2/PermissionsRequestApprovalParams.ts | 19 - .../v2/PermissionsRequestApprovalResponse.ts | 14 - .../generated/v2/PlanDeltaNotification.ts | 14 - .../openai/generated/v2/PluginAuthPolicy.ts | 5 - .../openai/generated/v2/PluginAvailability.ts | 5 - .../openai/generated/v2/PluginDetail.ts | 24 - .../generated/v2/PluginDisabledReason.ts | 9 - .../openai/generated/v2/PluginHookSummary.ts | 6 - .../generated/v2/PluginInstallParams.ts | 14 - .../generated/v2/PluginInstallPolicy.ts | 5 - .../generated/v2/PluginInstallPolicySource.ts | 5 - .../generated/v2/PluginInstallResponse.ts | 10 - .../generated/v2/PluginInstalledParams.ts | 16 - .../generated/v2/PluginInstalledResponse.ts | 10 - .../openai/generated/v2/PluginInterface.ts | 54 - .../generated/v2/PluginListMarketplaceKind.ts | 10 - .../openai/generated/v2/PluginListParams.ts | 22 - .../openai/generated/v2/PluginListResponse.ts | 11 - .../generated/v2/PluginMarketplaceEntry.ts | 17 - .../openai/generated/v2/PluginReadParams.ts | 10 - .../openai/generated/v2/PluginReadResponse.ts | 6 - .../openai/generated/v2/PluginSearchParams.ts | 13 - .../generated/v2/PluginSearchResponse.ts | 6 - .../openai/generated/v2/PluginSearchResult.ts | 11 - .../openai/generated/v2/PluginSearchScope.ts | 5 - .../generated/v2/PluginShareCheckoutParams.ts | 5 - .../v2/PluginShareCheckoutResponse.ts | 14 - .../openai/generated/v2/PluginShareContext.ts | 19 - .../generated/v2/PluginShareDeleteParams.ts | 5 - .../generated/v2/PluginShareDeleteResponse.ts | 5 - .../v2/PluginShareDiscoverability.ts | 5 - .../generated/v2/PluginShareListItem.ts | 10 - .../generated/v2/PluginShareListParams.ts | 5 - .../generated/v2/PluginShareListResponse.ts | 6 - .../generated/v2/PluginSharePrincipal.ts | 12 - .../generated/v2/PluginSharePrincipalRole.ts | 5 - .../generated/v2/PluginSharePrincipalType.ts | 5 - .../generated/v2/PluginShareSaveParams.ts | 13 - .../generated/v2/PluginShareSaveResponse.ts | 9 - .../openai/generated/v2/PluginShareTarget.ts | 11 - .../generated/v2/PluginShareTargetRole.ts | 5 - .../v2/PluginShareUpdateDiscoverability.ts | 5 - .../v2/PluginShareUpdateTargetsParams.ts | 11 - .../v2/PluginShareUpdateTargetsResponse.ts | 10 - .../generated/v2/PluginSkillReadParams.ts | 9 - .../generated/v2/PluginSkillReadResponse.ts | 5 - .../openai/generated/v2/PluginSource.ts | 21 - .../openai/generated/v2/PluginSummary.ts | 57 - .../generated/v2/PluginUninstallParams.ts | 5 - .../generated/v2/PluginUninstallResponse.ts | 5 - .../openai/generated/v2/PluginsMigration.ts | 5 - .../generated/v2/ProcessExitedNotification.ts | 43 - .../openai/generated/v2/ProcessKillParams.ts | 13 - .../generated/v2/ProcessKillResponse.ts | 8 - .../v2/ProcessOutputDeltaNotification.ts | 27 - .../generated/v2/ProcessOutputStream.ts | 8 - .../generated/v2/ProcessResizePtyParams.ts | 18 - .../generated/v2/ProcessResizePtyResponse.ts | 8 - .../openai/generated/v2/ProcessSpawnParams.ts | 74 -- .../generated/v2/ProcessSpawnResponse.ts | 8 - .../generated/v2/ProcessTerminalSize.ts | 17 - .../generated/v2/ProcessWriteStdinParams.ts | 22 - .../generated/v2/ProcessWriteStdinResponse.ts | 8 - src/runtimes/openai/generated/v2/Project.ts | 14 - .../openai/generated/v2/ProjectChangeType.ts | 5 - .../v2/ProjectChangedNotification.ts | 6 - .../generated/v2/ProjectCreateParams.ts | 11 - .../generated/v2/ProjectCreateResponse.ts | 6 - .../generated/v2/ProjectDeleteParams.ts | 5 - .../generated/v2/ProjectDeleteResponse.ts | 5 - .../generated/v2/ProjectImportParams.ts | 12 - .../generated/v2/ProjectImportResponse.ts | 6 - .../openai/generated/v2/ProjectListParams.ts | 5 - .../generated/v2/ProjectListResponse.ts | 6 - .../openai/generated/v2/ProjectMoveParams.ts | 5 - .../generated/v2/ProjectMoveResponse.ts | 5 - .../openai/generated/v2/ProjectReadParams.ts | 5 - .../generated/v2/ProjectReadResponse.ts | 6 - .../openai/generated/v2/ProjectRoot.ts | 6 - .../generated/v2/ProjectUpdateParams.ts | 11 - .../generated/v2/ProjectUpdateResponse.ts | 6 - .../openai/generated/v2/QueuedSubmission.ts | 6 - .../generated/v2/RateLimitReachedType.ts | 10 - .../generated/v2/RateLimitResetCredit.ts | 30 - .../v2/RateLimitResetCreditStatus.ts | 5 - .../v2/RateLimitResetCreditsSummary.ts | 16 - .../openai/generated/v2/RateLimitResetType.ts | 5 - .../openai/generated/v2/RateLimitSnapshot.ts | 23 - .../openai/generated/v2/RateLimitWindow.ts | 9 - .../v2/RawResponseCompletedNotification.ts | 15 - .../RawResponseItemCompletedNotification.ts | 10 - .../generated/v2/ReasoningEffortOption.ts | 6 - .../ReasoningSummaryPartAddedNotification.ts | 10 - .../ReasoningSummaryTextDeltaNotification.ts | 11 - .../v2/ReasoningTextDeltaNotification.ts | 11 - .../generated/v2/RemoteControlClient.ts | 14 - .../v2/RemoteControlClientsListOrder.ts | 5 - .../v2/RemoteControlClientsListParams.ts | 11 - .../v2/RemoteControlClientsListResponse.ts | 9 - .../v2/RemoteControlClientsRevokeParams.ts | 5 - .../v2/RemoteControlClientsRevokeResponse.ts | 5 - .../v2/RemoteControlConnectionStatus.ts | 5 - .../v2/RemoteControlDisableParams.ts | 5 - .../v2/RemoteControlDisableResponse.ts | 11 - .../generated/v2/RemoteControlEnableParams.ts | 5 - .../v2/RemoteControlEnableResponse.ts | 11 - .../v2/RemoteControlPairingStartParams.ts | 5 - .../v2/RemoteControlPairingStartResponse.ts | 10 - .../v2/RemoteControlPairingStatusParams.ts | 8 - .../v2/RemoteControlPairingStatusResponse.ts | 5 - .../RemoteControlStatusChangedNotification.ts | 14 - .../v2/RemoteControlStatusReadResponse.ts | 11 - .../generated/v2/RequestPermissionProfile.ts | 10 - .../generated/v2/ResidencyRequirement.ts | 5 - .../openai/generated/v2/ReviewDelivery.ts | 5 - .../openai/generated/v2/ReviewStartParams.ts | 15 - .../generated/v2/ReviewStartResponse.ts | 15 - .../openai/generated/v2/ReviewTarget.ts | 16 - .../generated/v2/SandboxWorkspaceWrite.ts | 10 - .../generated/v2/ScheduledTaskSchedule.ts | 10 - .../generated/v2/ScheduledTaskSummary.ts | 11 - .../generated/v2/ScheduledTaskWeekday.ts | 5 - .../v2/SendAddCreditsNudgeEmailParams.ts | 6 - .../v2/SendAddCreditsNudgeEmailResponse.ts | 6 - .../generated/v2/ServerDiagnosticsGauge.ts | 5 - .../generated/v2/ServerDiagnosticsParams.ts | 5 - .../generated/v2/ServerDiagnosticsProcess.ts | 9 - .../generated/v2/ServerDiagnosticsResponse.ts | 10 - .../v2/ServerRequestResolvedNotification.ts | 6 - .../openai/generated/v2/SessionMigration.ts | 5 - .../openai/generated/v2/SessionSource.ts | 13 - .../openai/generated/v2/SkillDependencies.ts | 6 - .../openai/generated/v2/SkillErrorInfo.ts | 5 - .../openai/generated/v2/SkillInterface.ts | 21 - .../openai/generated/v2/SkillMetadata.ts | 25 - .../openai/generated/v2/SkillMigration.ts | 5 - .../openai/generated/v2/SkillScope.ts | 5 - .../openai/generated/v2/SkillSummary.ts | 14 - .../generated/v2/SkillToolDependency.ts | 12 - .../generated/v2/SkillsChangedNotification.ts | 11 - .../generated/v2/SkillsConfigWriteParams.ts | 16 - .../generated/v2/SkillsConfigWriteResponse.ts | 5 - .../generated/v2/SkillsExtraRootsSetParams.ts | 6 - .../v2/SkillsExtraRootsSetResponse.ts | 5 - .../openai/generated/v2/SkillsListEntry.ts | 11 - .../openai/generated/v2/SkillsListParams.ts | 14 - .../openai/generated/v2/SkillsListResponse.ts | 6 - .../generated/v2/SpendControlLimitSnapshot.ts | 10 - .../v2/StrictReviewRequiredNotification.ts | 12 - .../generated/v2/SubAgentActivityKind.ts | 5 - .../openai/generated/v2/SubagentMigration.ts | 5 - .../v2/TerminalInteractionNotification.ts | 11 - .../openai/generated/v2/TextPosition.ts | 14 - src/runtimes/openai/generated/v2/TextRange.ts | 6 - src/runtimes/openai/generated/v2/Thread.ts | 127 --- .../openai/generated/v2/ThreadActiveFlag.ts | 5 - ...ThreadApproveGuardianDeniedActionParams.ts | 12 - ...readApproveGuardianDeniedActionResponse.ts | 5 - .../generated/v2/ThreadArchiveParams.ts | 5 - .../generated/v2/ThreadArchiveResponse.ts | 5 - .../v2/ThreadArchivedNotification.ts | 5 - .../generated/v2/ThreadBackgroundTerminal.ts | 14 - .../ThreadBackgroundTerminalsCleanResponse.ts | 5 - .../v2/ThreadBackgroundTerminalsListParams.ts | 15 - .../ThreadBackgroundTerminalsListResponse.ts | 13 - ...hreadBackgroundTerminalsTerminateParams.ts | 5 - ...eadBackgroundTerminalsTerminateResponse.ts | 5 - .../generated/v2/ThreadClosedNotification.ts | 5 - .../generated/v2/ThreadCompactStartParams.ts | 5 - .../v2/ThreadCompactStartResponse.ts | 5 - .../v2/ThreadDecrementElicitationParams.ts | 13 - .../v2/ThreadDecrementElicitationResponse.ts | 17 - .../openai/generated/v2/ThreadDeleteParams.ts | 5 - .../generated/v2/ThreadDeleteResponse.ts | 5 - .../generated/v2/ThreadDeletedNotification.ts | 5 - .../openai/generated/v2/ThreadExtra.ts | 8 - .../openai/generated/v2/ThreadForkParams.ts | 83 -- .../openai/generated/v2/ThreadForkResponse.ts | 49 - .../openai/generated/v2/ThreadGoal.ts | 15 - .../generated/v2/ThreadGoalClearParams.ts | 5 - .../generated/v2/ThreadGoalClearResponse.ts | 5 - .../v2/ThreadGoalClearedNotification.ts | 5 - .../generated/v2/ThreadGoalGetParams.ts | 5 - .../generated/v2/ThreadGoalGetResponse.ts | 6 - .../generated/v2/ThreadGoalSetParams.ts | 11 - .../generated/v2/ThreadGoalSetResponse.ts | 6 - .../openai/generated/v2/ThreadGoalStatus.ts | 11 - .../v2/ThreadGoalUpdatedNotification.ts | 10 - .../v2/ThreadIncrementElicitationParams.ts | 13 - .../v2/ThreadIncrementElicitationResponse.ts | 17 - .../generated/v2/ThreadInjectItemsResponse.ts | 5 - .../openai/generated/v2/ThreadItem.ts | 177 --- .../openai/generated/v2/ThreadItemEntry.ts | 12 - .../generated/v2/ThreadItemsListParams.ts | 24 - .../generated/v2/ThreadItemsListResponse.ts | 18 - .../openai/generated/v2/ThreadListParams.ts | 74 -- .../openai/generated/v2/ThreadListResponse.ts | 20 - .../generated/v2/ThreadLoadedListParams.ts | 14 - .../generated/v2/ThreadLoadedListResponse.ts | 15 - .../generated/v2/ThreadMemoryModeSetParams.ts | 6 - .../v2/ThreadMemoryModeSetResponse.ts | 5 - .../v2/ThreadMetadataGitInfoUpdateParams.ts | 21 - .../v2/ThreadMetadataUpdateParams.ts | 19 - .../v2/ThreadMetadataUpdateResponse.ts | 6 - .../v2/ThreadNameUpdatedNotification.ts | 5 - .../v2/ThreadProjectUpdatedNotification.ts | 5 - .../generated/v2/ThreadQueueAddParams.ts | 10 - .../generated/v2/ThreadQueueAddResponse.ts | 6 - .../v2/ThreadQueueChangedNotification.ts | 5 - .../generated/v2/ThreadQueueDeleteParams.ts | 5 - .../generated/v2/ThreadQueueDeleteResponse.ts | 5 - .../generated/v2/ThreadQueueListParams.ts | 15 - .../generated/v2/ThreadQueueListResponse.ts | 12 - .../generated/v2/ThreadQueueReorderParams.ts | 5 - .../v2/ThreadQueueReorderResponse.ts | 5 - .../generated/v2/ThreadQueueStartParams.ts | 5 - .../generated/v2/ThreadQueueStartResponse.ts | 6 - .../generated/v2/ThreadQueueUpdateParams.ts | 10 - .../generated/v2/ThreadQueueUpdateResponse.ts | 6 - .../openai/generated/v2/ThreadReadParams.ts | 11 - .../openai/generated/v2/ThreadReadResponse.ts | 6 - .../v2/ThreadRealtimeAppendAudioParams.ts | 9 - .../v2/ThreadRealtimeAppendAudioResponse.ts | 8 - .../v2/ThreadRealtimeAppendSpeechParams.ts | 8 - .../v2/ThreadRealtimeAppendSpeechResponse.ts | 8 - .../v2/ThreadRealtimeAppendTextParams.ts | 13 - .../v2/ThreadRealtimeAppendTextResponse.ts | 8 - .../generated/v2/ThreadRealtimeAudioChunk.ts | 14 - .../v2/ThreadRealtimeBemItemPresentation.ts | 11 - .../v2/ThreadRealtimeClosedNotification.ts | 8 - .../v2/ThreadRealtimeErrorNotification.ts | 8 - .../generated/v2/ThreadRealtimeInitialItem.ts | 9 - .../openai/generated/v2/ThreadRealtimeItem.ts | 21 - .../v2/ThreadRealtimeItemAddedNotification.ts | 9 - ...ThreadRealtimeItemCompletedNotification.ts | 12 - .../ThreadRealtimeItemStartedNotification.ts | 9 - ...RealtimeItemTranscriptDeltaNotification.ts | 12 - .../v2/ThreadRealtimeListVoicesParams.ts | 8 - .../v2/ThreadRealtimeListVoicesResponse.ts | 9 - ...eadRealtimeOutputAudioDeltaNotification.ts | 12 - .../v2/ThreadRealtimeSdpNotification.ts | 8 - .../v2/ThreadRealtimeSessionOutcome.ts | 5 - .../generated/v2/ThreadRealtimeStartParams.ts | 84 -- .../v2/ThreadRealtimeStartResponse.ts | 8 - .../v2/ThreadRealtimeStartTransport.ts | 24 - .../v2/ThreadRealtimeStartedNotification.ts | 13 - .../generated/v2/ThreadRealtimeStopParams.ts | 8 - .../v2/ThreadRealtimeStopResponse.ts | 8 - ...readRealtimeTranscriptDeltaNotification.ts | 16 - ...hreadRealtimeTranscriptDoneNotification.ts | 16 - .../v2/ThreadRealtimeTranscriptRole.ts | 5 - .../generated/v2/ThreadResumeResponse.ts | 68 -- .../openai/generated/v2/ThreadRevertParams.ts | 16 - .../generated/v2/ThreadRevertResponse.ts | 26 - .../v2/ThreadRevertedNotification.ts | 5 - .../generated/v2/ThreadRollbackParams.ts | 17 - .../generated/v2/ThreadRollbackResponse.ts | 15 - .../generated/v2/ThreadSearchOccurrence.ts | 21 - .../v2/ThreadSearchOccurrencesParams.ts | 23 - .../v2/ThreadSearchOccurrencesResponse.ts | 15 - .../openai/generated/v2/ThreadSearchParams.ts | 39 - .../generated/v2/ThreadSearchResponse.ts | 20 - .../openai/generated/v2/ThreadSearchResult.ts | 6 - .../generated/v2/ThreadSearchSortKey.ts | 5 - .../generated/v2/ThreadSearchTextRange.ts | 17 - .../openai/generated/v2/ThreadSection.ts | 22 - .../generated/v2/ThreadSectionAppearance.ts | 8 - .../generated/v2/ThreadSectionCreateParams.ts | 15 - .../v2/ThreadSectionCreateResponse.ts | 9 - .../generated/v2/ThreadSectionDeleteParams.ts | 13 - .../v2/ThreadSectionDeleteResponse.ts | 8 - .../generated/v2/ThreadSectionListParams.ts | 17 - .../generated/v2/ThreadSectionListResponse.ts | 15 - .../generated/v2/ThreadSectionMoveParams.ts | 21 - .../generated/v2/ThreadSectionMoveResponse.ts | 5 - .../generated/v2/ThreadSectionUpdateParams.ts | 22 - .../v2/ThreadSectionUpdateResponse.ts | 9 - .../generated/v2/ThreadSetNameParams.ts | 5 - .../generated/v2/ThreadSetNameResponse.ts | 5 - .../openai/generated/v2/ThreadSettings.ts | 32 - .../v2/ThreadSettingsUpdateParams.ts | 68 -- .../v2/ThreadSettingsUpdateResponse.ts | 5 - .../v2/ThreadSettingsUpdatedNotification.ts | 9 - .../generated/v2/ThreadShellCommandParams.ts | 14 - .../v2/ThreadShellCommandResponse.ts | 5 - .../openai/generated/v2/ThreadSortKey.ts | 5 - .../openai/generated/v2/ThreadSourceKind.ts | 15 - .../generated/v2/ThreadStartResponse.ts | 49 - .../generated/v2/ThreadStartedNotification.ts | 6 - .../openai/generated/v2/ThreadStatus.ts | 10 - .../v2/ThreadStatusChangedNotification.ts | 6 - .../generated/v2/ThreadTimelineEntry.ts | 25 - .../generated/v2/ThreadTimelineListParams.ts | 12 - .../v2/ThreadTimelineListResponse.ts | 13 - .../openai/generated/v2/ThreadTokenUsage.ts | 10 - .../v2/ThreadTokenUsageUpdatedNotification.ts | 10 - .../generated/v2/ThreadTurnsListParams.ts | 25 - .../generated/v2/ThreadTurnsListResponse.ts | 20 - .../generated/v2/ThreadUnarchiveParams.ts | 5 - .../generated/v2/ThreadUnarchiveResponse.ts | 6 - .../v2/ThreadUnarchivedNotification.ts | 5 - .../generated/v2/ThreadUnsubscribeParams.ts | 5 - .../generated/v2/ThreadUnsubscribeResponse.ts | 6 - .../generated/v2/ThreadUnsubscribeStatus.ts | 5 - .../openai/generated/v2/ThreadUsage.ts | 11 - .../generated/v2/ThreadUsageBreakdownGroup.ts | 15 - .../generated/v2/TokenUsageBreakdown.ts | 12 - .../v2/ToolRequestUserInputAnswer.ts | 8 - .../v2/ToolRequestUserInputOption.ts | 8 - .../v2/ToolRequestUserInputParams.ts | 19 - .../v2/ToolRequestUserInputQuestion.ts | 16 - .../v2/ToolRequestUserInputResponse.ts | 11 - src/runtimes/openai/generated/v2/ToolsV2.ts | 6 - src/runtimes/openai/generated/v2/Turn.ts | 39 - .../generated/v2/TurnCompletedNotification.ts | 6 - .../v2/TurnDiffUpdatedNotification.ts | 9 - src/runtimes/openai/generated/v2/TurnError.ts | 10 - .../generated/v2/TurnInterruptParams.ts | 5 - .../generated/v2/TurnInterruptResponse.ts | 5 - .../v2/TurnModerationMetadataNotification.ts | 10 - .../openai/generated/v2/TurnPlanStep.ts | 6 - .../openai/generated/v2/TurnPlanStepStatus.ts | 5 - .../v2/TurnPlanUpdatedNotification.ts | 11 - .../openai/generated/v2/TurnStartResponse.ts | 6 - .../generated/v2/TurnStartedNotification.ts | 6 - .../openai/generated/v2/TurnSteerParams.ts | 30 - .../openai/generated/v2/TurnSteerResponse.ts | 5 - src/runtimes/openai/generated/v2/TurnsPage.ts | 10 - .../generated/v2/WarningNotification.ts | 14 - .../openai/generated/v2/WebSearchAction.ts | 9 - .../generated/v2/WindowsSandboxReadiness.ts | 5 - .../v2/WindowsSandboxReadinessResponse.ts | 6 - ...indowsSandboxSetupCompletedNotification.ts | 10 - .../generated/v2/WindowsSandboxSetupMode.ts | 5 - .../v2/WindowsSandboxSetupStartParams.ts | 10 - .../v2/WindowsSandboxSetupStartResponse.ts | 5 - ...WindowsWorldWritableWarningNotification.ts | 9 - .../openai/generated/v2/WorkspaceMessage.ts | 18 - .../generated/v2/WorkspaceMessageType.ts | 5 - .../openai/generated/v2/WriteStatus.ts | 5 - src/runtimes/openai/generated/v2/index.ts | 714 ------------ src/runtimes/provider-options.ts | 25 +- tests/acp-client-request-handler.test.ts | 272 +---- tests/acp-configuration.test.ts | 44 +- tests/acp-driver-backend.test.ts | 127 +-- tests/acp-event-translator-lifecycle.test.ts | 129 +-- ...cp-event-translator-session-update.test.ts | 84 +- tests/acp-provider-fixtures.test.ts | 282 +---- tests/acp-session-setup.test.ts | 7 +- tests/acp-terminal-manager.test.ts | 44 +- tests/acp-test-helpers.ts | 27 + tests/agent-driver-kernel-async-queue.test.ts | 4 +- tests/architecture-boundaries.test.ts | 549 ++------- tests/claude-agent-sdk-durability.test.ts | 10 +- ...t-sdk-provider-fixtures-transcript.test.ts | 38 +- tests/claude-agent-sdk-task-events.test.ts | 229 ++-- tests/contract-wire.test.ts | 2 +- tests/driver-artifact-contract.test.ts | 131 +-- tests/driver-artifact-test-controller.test.ts | 81 ++ tests/driver-artifact-test-controller.ts | 41 +- tests/driver-boot-schema.test.ts | 270 +++++ tests/driver-golden-fixtures.test.ts | 45 + tests/driver-instance-socket-socket.test.ts | 75 ++ .../fixtures/architecture-boundaries-audit.ts | 22 - .../architecture-boundaries-resolution.ts | 25 - ...architecture-boundaries-runtime-loaders.ts | 26 - .../agent-tasks-replaced.json | 14 + .../agent-tasks-replaced.json | 24 + .../acp/cases/permission-request.json | 1 + .../providers/acp/cases/session-ready.json | 1 + .../cases/result-failure-diagnostic.json | 8 + .../stream-text-thinking-tool-result.json | 8 + .../cases/error-before-tracked-turn.json | 7 + .../cases/root-with-parallel-child-turns.json | 8 + ...rn-completed-with-final-agent-message.json | 7 + tests/fixtures/public-api-spaces-source.ts | 16 - tests/fixtures/public-api-type-namespace.ts | 10 - tests/fixtures/public-api-type-star-source.ts | 5 - ...penai-app-server-agent-task-events.test.ts | 198 ++++ ...enai-app-server-event-bridge-items.test.ts | 31 + ...-server-event-bridge-run-lifecycle.test.ts | 4 + ...i-app-server-event-bridge-terminal.test.ts | 4 + ...app-server-event-bridge-transcript.test.ts | 8 + ...penai-app-server-provider-fixtures.test.ts | 26 + tests/openai-app-server-startup.test.ts | 5 +- tests/opencode-acp-contract.test.ts | 2 +- tests/protocol-id.test.ts | 21 + tests/provider-fixture-test-helpers.ts | 28 +- tests/public-api-consumer.ts | 11 + tests/public-api-exports.snapshot.json | 563 --------- tests/public-api-exports.test.ts | 123 -- tests/public-api.test.ts | 25 +- vite.config.ts | 61 + 838 files changed, 3114 insertions(+), 15040 deletions(-) create mode 100644 scripts/prune-openai-generated-types.mjs delete mode 100644 src/protocol/boot/readers.ts delete mode 100644 src/protocol/host-integration.ts create mode 100644 src/runtimes/openai/app-server-agent-task-events.ts delete mode 100644 src/runtimes/openai/generated/AgentPath.ts delete mode 100644 src/runtimes/openai/generated/ApplyPatchApprovalParams.ts delete mode 100644 src/runtimes/openai/generated/ApplyPatchApprovalResponse.ts delete mode 100644 src/runtimes/openai/generated/AuthMode.ts delete mode 100644 src/runtimes/openai/generated/AutoCompactTokenLimitScope.ts delete mode 100644 src/runtimes/openai/generated/ClientNotification.ts delete mode 100644 src/runtimes/openai/generated/ClientRequest.ts delete mode 100644 src/runtimes/openai/generated/CodexResponseHandoffMode.ts delete mode 100644 src/runtimes/openai/generated/ConversationGitInfo.ts delete mode 100644 src/runtimes/openai/generated/ConversationSummary.ts delete mode 100644 src/runtimes/openai/generated/ConversationTextRole.ts delete mode 100644 src/runtimes/openai/generated/ExecCommandApprovalParams.ts delete mode 100644 src/runtimes/openai/generated/ExecCommandApprovalResponse.ts delete mode 100644 src/runtimes/openai/generated/ExecPolicyAmendment.ts delete mode 100644 src/runtimes/openai/generated/FileChange.ts delete mode 100644 src/runtimes/openai/generated/ForcedLoginMethod.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchMatchType.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchParams.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchResponse.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchResult.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionStartParams.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionStartResponse.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionStopParams.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionStopResponse.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateParams.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateResponse.ts delete mode 100644 src/runtimes/openai/generated/FuzzyFileSearchSessionUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/GetAuthStatusParams.ts delete mode 100644 src/runtimes/openai/generated/GetAuthStatusResponse.ts delete mode 100644 src/runtimes/openai/generated/GetConversationSummaryParams.ts delete mode 100644 src/runtimes/openai/generated/GetConversationSummaryResponse.ts delete mode 100644 src/runtimes/openai/generated/GitDiffToRemoteParams.ts delete mode 100644 src/runtimes/openai/generated/GitDiffToRemoteResponse.ts delete mode 100644 src/runtimes/openai/generated/GitSha.ts delete mode 100644 src/runtimes/openai/generated/ImageGenerationFailure.ts delete mode 100644 src/runtimes/openai/generated/ImageGenerationItem.ts delete mode 100644 src/runtimes/openai/generated/InitializeResponse.ts delete mode 100644 src/runtimes/openai/generated/InputModality.ts delete mode 100644 src/runtimes/openai/generated/InternalSessionSource.ts delete mode 100644 src/runtimes/openai/generated/McpServerInfo.ts delete mode 100644 src/runtimes/openai/generated/NetworkPolicyAmendment.ts delete mode 100644 src/runtimes/openai/generated/NetworkPolicyRuleAction.ts delete mode 100644 src/runtimes/openai/generated/ParsedCommand.ts delete mode 100644 src/runtimes/openai/generated/PathUri.ts delete mode 100644 src/runtimes/openai/generated/PlanType.ts create mode 100644 src/runtimes/openai/generated/ProtocolMethods.ts delete mode 100644 src/runtimes/openai/generated/RealtimeConversationVersion.ts delete mode 100644 src/runtimes/openai/generated/RealtimeOutputModality.ts delete mode 100644 src/runtimes/openai/generated/RealtimeVoice.ts delete mode 100644 src/runtimes/openai/generated/RealtimeVoicesList.ts delete mode 100644 src/runtimes/openai/generated/Resource.ts delete mode 100644 src/runtimes/openai/generated/ResourceContent.ts delete mode 100644 src/runtimes/openai/generated/ResourceTemplate.ts delete mode 100644 src/runtimes/openai/generated/ReviewDecision.ts delete mode 100644 src/runtimes/openai/generated/ServerNotification.ts delete mode 100644 src/runtimes/openai/generated/ServerNotificationEnvelope.ts delete mode 100644 src/runtimes/openai/generated/ServerRequest.ts delete mode 100644 src/runtimes/openai/generated/SessionSource.ts delete mode 100644 src/runtimes/openai/generated/SleepItem.ts delete mode 100644 src/runtimes/openai/generated/SubAgentSource.ts delete mode 100644 src/runtimes/openai/generated/ThreadId.ts delete mode 100644 src/runtimes/openai/generated/ThreadMemoryMode.ts delete mode 100644 src/runtimes/openai/generated/Tool.ts delete mode 100644 src/runtimes/openai/generated/Verbosity.ts delete mode 100644 src/runtimes/openai/generated/WebSearchContextSize.ts delete mode 100644 src/runtimes/openai/generated/WebSearchItem.ts delete mode 100644 src/runtimes/openai/generated/WebSearchLocation.ts delete mode 100644 src/runtimes/openai/generated/WebSearchMode.ts delete mode 100644 src/runtimes/openai/generated/WebSearchToolConfig.ts delete mode 100644 src/runtimes/openai/generated/index.ts delete mode 100644 src/runtimes/openai/generated/v2/Account.ts delete mode 100644 src/runtimes/openai/generated/v2/AccountLoginCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/AccountRateLimitsUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/AccountTokenUsageDailyBucket.ts delete mode 100644 src/runtimes/openai/generated/v2/AccountTokenUsageSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/AccountUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ActivePermissionProfile.ts delete mode 100644 src/runtimes/openai/generated/v2/AddCreditsNudgeCreditType.ts delete mode 100644 src/runtimes/openai/generated/v2/AddCreditsNudgeEmailStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/AdditionalFileSystemPermissions.ts delete mode 100644 src/runtimes/openai/generated/v2/AdditionalNetworkPermissions.ts delete mode 100644 src/runtimes/openai/generated/v2/AdditionalPermissionProfile.ts delete mode 100644 src/runtimes/openai/generated/v2/AgentMessageDelivery.ts delete mode 100644 src/runtimes/openai/generated/v2/AgentMessageDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/AllowDenyRequirement.ts delete mode 100644 src/runtimes/openai/generated/v2/AnalyticsConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/AppBranding.ts delete mode 100644 src/runtimes/openai/generated/v2/AppInfo.ts delete mode 100644 src/runtimes/openai/generated/v2/AppListUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/AppMetadata.ts delete mode 100644 src/runtimes/openai/generated/v2/AppReview.ts delete mode 100644 src/runtimes/openai/generated/v2/AppScreenshot.ts delete mode 100644 src/runtimes/openai/generated/v2/AppSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/AppTemplateSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/AppTemplateUnavailableReason.ts delete mode 100644 src/runtimes/openai/generated/v2/AppToolApproval.ts delete mode 100644 src/runtimes/openai/generated/v2/AppToolSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/AppToolsConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/AppsConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/AppsDefaultConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/AppsInstalledParams.ts delete mode 100644 src/runtimes/openai/generated/v2/AppsInstalledResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/AppsListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/AppsListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/AppsReadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/AppsReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/AttestationGenerateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/AttestationGenerateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/AutoReviewDecisionSource.ts delete mode 100644 src/runtimes/openai/generated/v2/AutoReviewRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/AwsCredentialType.ts delete mode 100644 src/runtimes/openai/generated/v2/BedrockAwsProfile.ts delete mode 100644 src/runtimes/openai/generated/v2/BedrockDiscoverParams.ts delete mode 100644 src/runtimes/openai/generated/v2/BedrockDiscoverResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/BedrockEnvironmentCredential.ts delete mode 100644 src/runtimes/openai/generated/v2/BedrockSetupParams.ts delete mode 100644 src/runtimes/openai/generated/v2/BedrockSetupResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/BrowserUseAccessApprovalLifetime.ts delete mode 100644 src/runtimes/openai/generated/v2/BrowserUseConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/BrowserUseOriginPolicy.ts delete mode 100644 src/runtimes/openai/generated/v2/BrowserUseOriginPolicyConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/BrowserUseRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/CancelLoginAccountParams.ts delete mode 100644 src/runtimes/openai/generated/v2/CancelLoginAccountResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/CancelLoginAccountStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshReason.ts delete mode 100644 src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/CliAuthCredentialsStoreMode.ts delete mode 100644 src/runtimes/openai/generated/v2/CodexErrorInfo.ts delete mode 100644 src/runtimes/openai/generated/v2/CollabAgentState.ts delete mode 100644 src/runtimes/openai/generated/v2/CollabAgentStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/CollabAgentTool.ts delete mode 100644 src/runtimes/openai/generated/v2/CollabAgentToolCallStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/CollaborationModeListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/CollaborationModeListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/CollaborationModeMask.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandAction.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecOutputDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecOutputStream.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecParams.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecResizeParams.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecResizeResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecTerminalSize.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecTerminateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecTerminateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecWriteParams.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecWriteResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecutionApprovalDecision.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecutionApprovalKind.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecutionOutputDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalParams.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecutionSource.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandExecutionStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/CommandMigration.ts delete mode 100644 src/runtimes/openai/generated/v2/ComputerUseConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/ComputerUseMacosConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/ComputerUseMacosRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/ComputerUseRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/ComputerUseWindowsConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/ComputerUseWindowsExeConfig.ts delete mode 100644 src/runtimes/openai/generated/v2/ComputerUseWindowsExeRequirement.ts delete mode 100644 src/runtimes/openai/generated/v2/ComputerUseWindowsRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/Config.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigBatchWriteParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigEdit.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigLayer.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigLayerMetadata.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigLayerSource.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigReadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigRequirementsReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigValueWriteParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigWarningNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfigWriteResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfiguredHookHandler.ts delete mode 100644 src/runtimes/openai/generated/v2/ConfiguredHookMatcherGroup.ts delete mode 100644 src/runtimes/openai/generated/v2/ConnectorMetadata.ts delete mode 100644 src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditOutcome.ts delete mode 100644 src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ContextCompactedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/CreditsSnapshot.ts delete mode 100644 src/runtimes/openai/generated/v2/CurrentTimeReadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/DeprecationNoticeNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/DesktopOnboardingEntrypoint.ts delete mode 100644 src/runtimes/openai/generated/v2/DynamicToolCallOutputContentItem.ts delete mode 100644 src/runtimes/openai/generated/v2/DynamicToolCallParams.ts delete mode 100644 src/runtimes/openai/generated/v2/DynamicToolCallResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/DynamicToolCallStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/EnvironmentAddParams.ts delete mode 100644 src/runtimes/openai/generated/v2/EnvironmentAddResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/EnvironmentConnectionNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/EnvironmentInfoParams.ts delete mode 100644 src/runtimes/openai/generated/v2/EnvironmentInfoResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/EnvironmentShellInfo.ts delete mode 100644 src/runtimes/openai/generated/v2/EnvironmentStatusKind.ts delete mode 100644 src/runtimes/openai/generated/v2/EnvironmentStatusParams.ts delete mode 100644 src/runtimes/openai/generated/v2/EnvironmentStatusResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ErrorNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ExecPolicyAmendment.ts delete mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeature.ts delete mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ExperimentalFeatureStage.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigDetectParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigDetectResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistory.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordSuccessParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordTypeResultParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeFailure.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeSuccess.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportProgressNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigImportTypeResult.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItem.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItemType.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorCandidate.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorSource.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorCandidate.ts delete mode 100644 src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorSource.ts delete mode 100644 src/runtimes/openai/generated/v2/FeedbackRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/FeedbackUploadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FeedbackUploadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FileChangeApprovalDecision.ts delete mode 100644 src/runtimes/openai/generated/v2/FileChangeOutputDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/FileChangePatchUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/FileChangeRequestApprovalParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FileChangeRequestApprovalResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FileSystemAccessMode.ts delete mode 100644 src/runtimes/openai/generated/v2/FileSystemPath.ts delete mode 100644 src/runtimes/openai/generated/v2/FileSystemSandboxEntry.ts delete mode 100644 src/runtimes/openai/generated/v2/FileSystemSpecialPath.ts delete mode 100644 src/runtimes/openai/generated/v2/FileUpdateChange.ts delete mode 100644 src/runtimes/openai/generated/v2/ForcedChatgptWorkspaceIds.ts delete mode 100644 src/runtimes/openai/generated/v2/FsChangedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/FsCopyParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FsCopyResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FsCreateDirectoryParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FsCreateDirectoryResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FsGetMetadataParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FsGetMetadataResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FsReadDirectoryEntry.ts delete mode 100644 src/runtimes/openai/generated/v2/FsReadDirectoryParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FsReadDirectoryResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FsReadFileParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FsReadFileResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FsRemoveParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FsRemoveResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FsUnwatchParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FsUnwatchResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FsWatchParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FsWatchResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/FsWriteFileParams.ts delete mode 100644 src/runtimes/openai/generated/v2/FsWriteFileResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/GetAccountParams.ts delete mode 100644 src/runtimes/openai/generated/v2/GetAccountRateLimitsResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/GetAccountResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/GetAccountTokenUsageParams.ts delete mode 100644 src/runtimes/openai/generated/v2/GetAccountTokenUsageResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/GetWorkspaceMessagesResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/GitInfo.ts delete mode 100644 src/runtimes/openai/generated/v2/GrantedPermissionProfile.ts delete mode 100644 src/runtimes/openai/generated/v2/GuardianApprovalReview.ts delete mode 100644 src/runtimes/openai/generated/v2/GuardianApprovalReviewAction.ts delete mode 100644 src/runtimes/openai/generated/v2/GuardianApprovalReviewStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/GuardianCommandSource.ts delete mode 100644 src/runtimes/openai/generated/v2/GuardianRiskLevel.ts delete mode 100644 src/runtimes/openai/generated/v2/GuardianUserAuthorization.ts delete mode 100644 src/runtimes/openai/generated/v2/GuardianWarningNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/HookCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/HookErrorInfo.ts delete mode 100644 src/runtimes/openai/generated/v2/HookEventName.ts delete mode 100644 src/runtimes/openai/generated/v2/HookExecutionMode.ts delete mode 100644 src/runtimes/openai/generated/v2/HookHandlerType.ts delete mode 100644 src/runtimes/openai/generated/v2/HookMetadata.ts delete mode 100644 src/runtimes/openai/generated/v2/HookMigration.ts delete mode 100644 src/runtimes/openai/generated/v2/HookOutputEntry.ts delete mode 100644 src/runtimes/openai/generated/v2/HookOutputEntryKind.ts delete mode 100644 src/runtimes/openai/generated/v2/HookPromptFragment.ts delete mode 100644 src/runtimes/openai/generated/v2/HookRunStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/HookRunSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/HookScope.ts delete mode 100644 src/runtimes/openai/generated/v2/HookSource.ts delete mode 100644 src/runtimes/openai/generated/v2/HookStartedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/HookTrustStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/HooksListEntry.ts delete mode 100644 src/runtimes/openai/generated/v2/HooksListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/HooksListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/InAppBrowserRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/InstalledApp.ts delete mode 100644 src/runtimes/openai/generated/v2/ItemCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewStartedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ItemStartedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ListMcpServerStatusParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ListMcpServerStatusResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/LoginAccountParams.ts delete mode 100644 src/runtimes/openai/generated/v2/LoginAccountResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/LoginAppBrand.ts delete mode 100644 src/runtimes/openai/generated/v2/LogoutAccountResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ManagedHooksRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/MarketplaceAddParams.ts delete mode 100644 src/runtimes/openai/generated/v2/MarketplaceAddResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/MarketplaceInterface.ts delete mode 100644 src/runtimes/openai/generated/v2/MarketplaceLoadErrorInfo.ts delete mode 100644 src/runtimes/openai/generated/v2/MarketplaceRemoveParams.ts delete mode 100644 src/runtimes/openai/generated/v2/MarketplaceRemoveResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/MarketplaceUpgradeErrorInfo.ts delete mode 100644 src/runtimes/openai/generated/v2/MarketplaceUpgradeParams.ts delete mode 100644 src/runtimes/openai/generated/v2/MarketplaceUpgradeResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/McpAuthStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationArrayType.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationBooleanSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationBooleanType.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationConstOption.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationEnumSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationLegacyTitledEnumSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationMultiSelectEnumSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationNumberSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationNumberType.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationObjectType.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationPrimitiveSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationSingleSelectEnumSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationStringFormat.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationStringSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationStringType.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationTitledEnumItems.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationTitledMultiSelectEnumSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationTitledSingleSelectEnumSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationUntitledEnumItems.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationUntitledMultiSelectEnumSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpElicitationUntitledSingleSelectEnumSchema.ts delete mode 100644 src/runtimes/openai/generated/v2/McpResourceReadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/McpResourceReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerConnectionStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerElicitationAction.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerElicitationRequestParams.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerElicitationRequestResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerEventNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamStartParams.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamStartResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamStopParams.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerEventStreamStopResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerMigration.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerOauthClientRegistration.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerOauthLoginCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerOauthLoginParams.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerOauthLoginResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerRefreshResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerStartupFailureReason.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerStartupState.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerStatusDetail.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerStatusUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerToolCallParams.ts delete mode 100644 src/runtimes/openai/generated/v2/McpServerToolCallResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/McpToolCallAppContext.ts delete mode 100644 src/runtimes/openai/generated/v2/McpToolCallError.ts delete mode 100644 src/runtimes/openai/generated/v2/McpToolCallProgressNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/McpToolCallResult.ts delete mode 100644 src/runtimes/openai/generated/v2/McpToolCallStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/MemoryCitation.ts delete mode 100644 src/runtimes/openai/generated/v2/MemoryCitationEntry.ts delete mode 100644 src/runtimes/openai/generated/v2/MemoryResetResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/MergeStrategy.ts delete mode 100644 src/runtimes/openai/generated/v2/MigrationDetails.ts delete mode 100644 src/runtimes/openai/generated/v2/MockExperimentalMethodParams.ts delete mode 100644 src/runtimes/openai/generated/v2/MockExperimentalMethodResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/Model.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelAvailabilityNux.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelRerouteReason.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelReroutedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelSafetyBufferingUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelServiceTier.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelUpgradeInfo.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelVerification.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelVerificationNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ModelsRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/MultiAgentVersion.ts delete mode 100644 src/runtimes/openai/generated/v2/NetworkApprovalContext.ts delete mode 100644 src/runtimes/openai/generated/v2/NetworkApprovalProtocol.ts delete mode 100644 src/runtimes/openai/generated/v2/NetworkDomainPermission.ts delete mode 100644 src/runtimes/openai/generated/v2/NetworkPolicyAmendment.ts delete mode 100644 src/runtimes/openai/generated/v2/NetworkPolicyRuleAction.ts delete mode 100644 src/runtimes/openai/generated/v2/NetworkRequirements.ts delete mode 100644 src/runtimes/openai/generated/v2/NetworkUnixSocketPermission.ts delete mode 100644 src/runtimes/openai/generated/v2/NewThreadModelDefaults.ts delete mode 100644 src/runtimes/openai/generated/v2/NonSteerableTurnKind.ts delete mode 100644 src/runtimes/openai/generated/v2/OverriddenMetadata.ts delete mode 100644 src/runtimes/openai/generated/v2/PatchApplyStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/PatchChangeKind.ts delete mode 100644 src/runtimes/openai/generated/v2/PermissionGrantScope.ts delete mode 100644 src/runtimes/openai/generated/v2/PermissionProfileListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PermissionProfileListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PermissionProfileSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/PermissionsRequestApprovalParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PermissionsRequestApprovalResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PlanDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginAuthPolicy.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginAvailability.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginDetail.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginDisabledReason.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginHookSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginInstallParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginInstallPolicy.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginInstallPolicySource.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginInstallResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginInstalledParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginInstalledResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginInterface.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginListMarketplaceKind.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginMarketplaceEntry.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginReadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSearchParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSearchResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSearchResult.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSearchScope.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareCheckoutParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareCheckoutResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareContext.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareDeleteParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareDeleteResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareDiscoverability.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareListItem.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSharePrincipal.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSharePrincipalRole.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSharePrincipalType.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareSaveParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareSaveResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareTarget.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareTargetRole.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareUpdateDiscoverability.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareUpdateTargetsParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginShareUpdateTargetsResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSkillReadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSkillReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSource.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginUninstallParams.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginUninstallResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/PluginsMigration.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessExitedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessKillParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessKillResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessOutputDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessOutputStream.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessResizePtyParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessResizePtyResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessSpawnParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessSpawnResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessTerminalSize.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessWriteStdinParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProcessWriteStdinResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/Project.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectChangeType.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectChangedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectCreateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectCreateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectDeleteParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectDeleteResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectImportParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectImportResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectMoveParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectMoveResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectReadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectRoot.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectUpdateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ProjectUpdateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/QueuedSubmission.ts delete mode 100644 src/runtimes/openai/generated/v2/RateLimitReachedType.ts delete mode 100644 src/runtimes/openai/generated/v2/RateLimitResetCredit.ts delete mode 100644 src/runtimes/openai/generated/v2/RateLimitResetCreditStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/RateLimitResetCreditsSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/RateLimitResetType.ts delete mode 100644 src/runtimes/openai/generated/v2/RateLimitSnapshot.ts delete mode 100644 src/runtimes/openai/generated/v2/RateLimitWindow.ts delete mode 100644 src/runtimes/openai/generated/v2/RawResponseCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/RawResponseItemCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ReasoningEffortOption.ts delete mode 100644 src/runtimes/openai/generated/v2/ReasoningSummaryPartAddedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ReasoningSummaryTextDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ReasoningTextDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlClient.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsListOrder.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsRevokeParams.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlClientsRevokeResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlConnectionStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlDisableParams.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlDisableResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlEnableParams.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlEnableResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlPairingStartParams.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlPairingStartResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlPairingStatusParams.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlPairingStatusResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlStatusChangedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/RemoteControlStatusReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/RequestPermissionProfile.ts delete mode 100644 src/runtimes/openai/generated/v2/ResidencyRequirement.ts delete mode 100644 src/runtimes/openai/generated/v2/ReviewDelivery.ts delete mode 100644 src/runtimes/openai/generated/v2/ReviewStartParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ReviewStartResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ReviewTarget.ts delete mode 100644 src/runtimes/openai/generated/v2/SandboxWorkspaceWrite.ts delete mode 100644 src/runtimes/openai/generated/v2/ScheduledTaskSchedule.ts delete mode 100644 src/runtimes/openai/generated/v2/ScheduledTaskSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/ScheduledTaskWeekday.ts delete mode 100644 src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailParams.ts delete mode 100644 src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ServerDiagnosticsGauge.ts delete mode 100644 src/runtimes/openai/generated/v2/ServerDiagnosticsParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ServerDiagnosticsProcess.ts delete mode 100644 src/runtimes/openai/generated/v2/ServerDiagnosticsResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ServerRequestResolvedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/SessionMigration.ts delete mode 100644 src/runtimes/openai/generated/v2/SessionSource.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillDependencies.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillErrorInfo.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillInterface.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillMetadata.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillMigration.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillScope.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillSummary.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillToolDependency.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillsChangedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillsConfigWriteParams.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillsConfigWriteResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillsExtraRootsSetParams.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillsExtraRootsSetResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillsListEntry.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillsListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/SkillsListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/SpendControlLimitSnapshot.ts delete mode 100644 src/runtimes/openai/generated/v2/StrictReviewRequiredNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/SubAgentActivityKind.ts delete mode 100644 src/runtimes/openai/generated/v2/SubagentMigration.ts delete mode 100644 src/runtimes/openai/generated/v2/TerminalInteractionNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/TextPosition.ts delete mode 100644 src/runtimes/openai/generated/v2/TextRange.ts delete mode 100644 src/runtimes/openai/generated/v2/Thread.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadActiveFlag.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadArchiveParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadArchiveResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadArchivedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminal.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadClosedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadCompactStartParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadCompactStartResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadDecrementElicitationParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadDecrementElicitationResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadDeleteParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadDeleteResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadDeletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadExtra.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadForkParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadForkResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoal.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoalClearParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoalClearResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoalClearedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoalGetParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoalGetResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoalSetParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoalSetResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoalStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadGoalUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadIncrementElicitationParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadIncrementElicitationResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadInjectItemsResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadItem.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadItemEntry.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadItemsListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadItemsListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadLoadedListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadLoadedListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadMemoryModeSetParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadMemoryModeSetResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadMetadataGitInfoUpdateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadMetadataUpdateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadMetadataUpdateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadNameUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadProjectUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueAddParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueAddResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueChangedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueDeleteParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueDeleteResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueReorderParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueReorderResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueStartParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueStartResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueUpdateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadQueueUpdateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadReadParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadReadResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeAudioChunk.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeBemItemPresentation.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeClosedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeErrorNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeInitialItem.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItem.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItemAddedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItemCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItemStartedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeItemTranscriptDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeOutputAudioDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeSdpNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeSessionOutcome.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStartParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStartResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStartTransport.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStartedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStopParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeStopResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDeltaNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDoneNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptRole.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadResumeResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRevertParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRevertResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRevertedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRollbackParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadRollbackResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSearchOccurrence.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSearchOccurrencesParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSearchOccurrencesResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSearchParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSearchResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSearchResult.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSearchSortKey.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSearchTextRange.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSection.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionAppearance.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionCreateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionCreateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionDeleteParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionDeleteResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionMoveParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionMoveResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionUpdateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSectionUpdateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSetNameParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSetNameResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSettings.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSettingsUpdateParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSettingsUpdateResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSettingsUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadShellCommandParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadShellCommandResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSortKey.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadSourceKind.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadStartResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadStartedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadStatusChangedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadTimelineEntry.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadTimelineListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadTimelineListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadTokenUsage.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadTokenUsageUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadTurnsListParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadTurnsListResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadUnarchiveParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadUnarchiveResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadUnarchivedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadUnsubscribeParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadUnsubscribeResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadUnsubscribeStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadUsage.ts delete mode 100644 src/runtimes/openai/generated/v2/ThreadUsageBreakdownGroup.ts delete mode 100644 src/runtimes/openai/generated/v2/TokenUsageBreakdown.ts delete mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputAnswer.ts delete mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputOption.ts delete mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputParams.ts delete mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputQuestion.ts delete mode 100644 src/runtimes/openai/generated/v2/ToolRequestUserInputResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/ToolsV2.ts delete mode 100644 src/runtimes/openai/generated/v2/Turn.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnDiffUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnError.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnInterruptParams.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnInterruptResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnModerationMetadataNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnPlanStep.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnPlanStepStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnPlanUpdatedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnStartResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnStartedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnSteerParams.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnSteerResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/TurnsPage.ts delete mode 100644 src/runtimes/openai/generated/v2/WarningNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/WebSearchAction.ts delete mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxReadiness.ts delete mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxReadinessResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxSetupCompletedNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxSetupMode.ts delete mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxSetupStartParams.ts delete mode 100644 src/runtimes/openai/generated/v2/WindowsSandboxSetupStartResponse.ts delete mode 100644 src/runtimes/openai/generated/v2/WindowsWorldWritableWarningNotification.ts delete mode 100644 src/runtimes/openai/generated/v2/WorkspaceMessage.ts delete mode 100644 src/runtimes/openai/generated/v2/WorkspaceMessageType.ts delete mode 100644 src/runtimes/openai/generated/v2/WriteStatus.ts delete mode 100644 src/runtimes/openai/generated/v2/index.ts create mode 100644 tests/acp-test-helpers.ts create mode 100644 tests/driver-boot-schema.test.ts delete mode 100644 tests/fixtures/architecture-boundaries-audit.ts delete mode 100644 tests/fixtures/architecture-boundaries-resolution.ts delete mode 100644 tests/fixtures/architecture-boundaries-runtime-loaders.ts create mode 100644 tests/fixtures/driver/runtime-event-drafts/agent-tasks-replaced.json create mode 100644 tests/fixtures/driver/runtime-event-envelopes/agent-tasks-replaced.json delete mode 100644 tests/fixtures/public-api-spaces-source.ts delete mode 100644 tests/fixtures/public-api-type-namespace.ts delete mode 100644 tests/fixtures/public-api-type-star-source.ts create mode 100644 tests/openai-app-server-agent-task-events.test.ts create mode 100644 tests/protocol-id.test.ts delete mode 100644 tests/public-api-exports.snapshot.json delete mode 100644 tests/public-api-exports.test.ts diff --git a/bench/ttft-bench.ts b/bench/ttft-bench.ts index 144581e..b8ecaed 100644 --- a/bench/ttft-bench.ts +++ b/bench/ttft-bench.ts @@ -31,10 +31,8 @@ import { fileURLToPath } from "node:url"; import { AgentDriverKernelCore } from "../src/core/agent-driver-kernel"; import type { PermissionDecision } from "../src/core/driver-permission-broker"; import type { DriverEventInput } from "../src/protocol/events"; -import { createDriverHostIntegrationSnapshotFromBootExecution } from "../src/protocol/host-integration"; import type { DriverStartInput } from "../src/protocol/start"; import { AGENT_DRIVER_PROVIDER_REGISTRY } from "../src/runtimes/provider-registry"; -import { driverBootPayload } from "../tests/driver-boot-payload-fixture"; import { DRIVER_TEST_IDS, bootPayload } from "../tests/driver-runtime-boundary-fixtures"; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -176,6 +174,11 @@ function claudeStartInput(a: StartInputArgs): DriverStartInput { session: { ...bootPayload.execution.session, additionalDirectories: [], + context: { + ...bootPayload.execution.session.context, + homePath: a.homePath, + sessionOrganizationPath: a.sharedRootPath, + }, cwd: a.cwd, homePath: a.homePath, mcpServers: [], @@ -204,6 +207,11 @@ function openaiStartInput(a: StartInputArgs): DriverStartInput { session: { ...bootPayload.execution.session, additionalDirectories: [], + context: { + ...bootPayload.execution.session.context, + homePath: a.homePath, + sessionOrganizationPath: a.sharedRootPath, + }, cwd: a.cwd, homePath: a.homePath, mcpServers: [], @@ -250,6 +258,11 @@ function opencodeStartInput( session: { ...bootPayload.execution.session, additionalDirectories: [], + context: { + ...bootPayload.execution.session.context, + homePath: a.homePath, + sessionOrganizationPath: a.sharedRootPath, + }, cwd: a.cwd, homePath: a.homePath, mcpServers: [], @@ -265,34 +278,12 @@ function opencodeStartInput( }; } -function opencodeHostSnapshot(paths: { cwd: string; homePath: string; sharedRootPath: string }) { - return createDriverHostIntegrationSnapshotFromBootExecution({ - ...driverBootPayload.execution, - profilePrompt: "", - session: { - ...driverBootPayload.execution.session, - additionalDirectories: [], - context: { - ...driverBootPayload.execution.session.context, - homePath: paths.homePath, - sessionOrganizationPath: paths.sharedRootPath, - }, - cwd: paths.cwd, - mcpServers: [], - nativeResumeRef: null, - }, - skillCatalog: [], - skills: [], - }); -} - async function runTrial(input: { runtime: RuntimeId; scenario: Scenario; startInput: (paths: StartInputArgs) => DriverStartInput; apiKey: string; model: string; - isOpenCode: boolean; }): Promise { const paths = await makePaths(`ttft-${input.runtime}-`); const args: StartInputArgs = { @@ -303,13 +294,11 @@ async function runTrial(input: { model: input.model, systemPrompt: input.scenario.systemPrompt, }; - const hostSnapshot = input.isOpenCode ? opencodeHostSnapshot(paths) : null; const kernel = new AgentDriverKernelCore({ backendFactory: (i) => AGENT_DRIVER_PROVIDER_REGISTRY.createBackend(i), hostPorts: { permission: { request: async () => input.scenario.permission }, skill: { materialize: async () => [] }, - ...(hostSnapshot === null ? {} : { hostIntegration: { snapshot: async () => hostSnapshot } }), }, }); const events = kernel.events(); @@ -500,15 +489,12 @@ async function main(): Promise { model: string, build: (paths: StartInputArgs) => DriverStartInput, apiKey: string, - isOpenCode: boolean, ): Promise => { process.stdout.write(`\n[${runtime}/${scenario.id}] model=${model} warmup...`); - await runTrial({ runtime, scenario, startInput: build, apiKey, model, isOpenCode }).catch( - () => undefined, - ); + await runTrial({ runtime, scenario, startInput: build, apiKey, model }).catch(() => undefined); const results: TrialMetrics[] = []; for (let i = 0; i < trials; i += 1) { - const m = await runTrial({ runtime, scenario, startInput: build, apiKey, model, isOpenCode }); + const m = await runTrial({ runtime, scenario, startInput: build, apiKey, model }); results.push(m); process.stdout.write( ` t${i + 1}=${m.ok ? "ok" : "FAIL"}(ttft=${m.ttftMs ?? "-"},total=${m.totalMs ?? "-"})`, @@ -519,17 +505,10 @@ async function main(): Promise { for (const scenario of SCENARIOS.filter((s) => scenarioFilter.has(s.id))) { if (requested.includes("claude") && anthropicKey) { - await runCell( - "claude", - scenario, - claudeModel, - (p) => claudeStartInput(p), - anthropicKey, - false, - ); + await runCell("claude", scenario, claudeModel, (p) => claudeStartInput(p), anthropicKey); } if (requested.includes("openai") && openaiKey) { - await runCell("openai", scenario, openaiModel, (p) => openaiStartInput(p), openaiKey, false); + await runCell("openai", scenario, openaiModel, (p) => openaiStartInput(p), openaiKey); } if (requested.includes("opencode")) { const key = opencodeProvider === "anthropic" ? anthropicKey : openaiKey; @@ -546,7 +525,6 @@ async function main(): Promise { model, (p) => opencodeStartInput(p, opencodeProvider, apiKeyEnv), key, - true, ); } } diff --git a/scripts/prune-openai-generated-types.mjs b/scripts/prune-openai-generated-types.mjs new file mode 100644 index 0000000..28a8127 --- /dev/null +++ b/scripts/prune-openai-generated-types.mjs @@ -0,0 +1,100 @@ +import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, relative, resolve, sep } from "node:path"; + +const generatedDir = resolve("src/runtimes/openai/generated"); +const schemaDir = resolve("src/runtimes/openai/generated-json-schema"); +const protocolTypesPath = resolve("src/runtimes/openai/app-server-protocol-types.ts"); +const methodsPath = resolve(generatedDir, "ProtocolMethods.ts"); + +function readMethods(schemaName) { + const schema = JSON.parse(readFileSync(resolve(schemaDir, `${schemaName}.json`), "utf8")); + if (!Array.isArray(schema.oneOf)) { + throw new TypeError(`${schemaName}.json must contain a oneOf array.`); + } + + const methods = schema.oneOf.map((branch, index) => { + const values = branch?.properties?.method?.enum; + if (values?.length !== 1 || typeof values[0] !== "string") { + throw new TypeError(`${schemaName}.json oneOf[${index}] must have one method enum value.`); + } + return values[0]; + }); + + if (new Set(methods).size !== methods.length) { + throw new TypeError(`${schemaName}.json contains duplicate methods.`); + } + return methods; +} + +function renderUnion(name, methods) { + return `export type ${name} =\n${methods.map((method) => ` | ${JSON.stringify(method)}`).join("\n")};`; +} + +writeFileSync( + methodsPath, + [ + "// GENERATED CODE! DO NOT MODIFY BY HAND!", + "", + "// Derived from the matching runtime JSON Schemas by scripts/prune-openai-generated-types.mjs.", + renderUnion("ServerNotificationMethod", readMethods("ServerNotification")), + "", + renderUnion("ServerRequestMethod", readMethods("ServerRequest")), + "", + ].join("\n"), +); + +function resolveImport(importer, specifier) { + const unresolved = resolve(dirname(importer), specifier); + const candidates = unresolved.endsWith(".ts") + ? [unresolved] + : [`${unresolved}.ts`, resolve(unresolved, "index.ts")]; + const file = candidates.find(existsSync); + if (file === undefined) { + throw new Error(`${relative(generatedDir, importer)} imports missing module ${specifier}.`); + } + + const path = relative(generatedDir, file); + if (path === ".." || path.startsWith(`..${sep}`)) { + throw new Error(`${relative(generatedDir, importer)} imports outside the generated directory.`); + } + return file; +} + +const protocolSource = readFileSync(protocolTypesPath, "utf8"); +const protocolImportPattern = /\bfrom\s+["'](\.\/generated(?:\/[^"']+)?)['"]/gu; +const pending = [...protocolSource.matchAll(protocolImportPattern)].map((match) => + resolveImport(protocolTypesPath, match[1]), +); +if (pending.length === 0) { + throw new Error("app-server-protocol-types.ts must import at least one generated type."); +} +const reachable = new Set(); +const relativeImportPattern = /\b(?:from\s+|import\s*\()\s*["'](\.[^"']+)["']/gu; + +while (pending.length > 0) { + const file = pending.pop(); + if (file === undefined || reachable.has(file)) { + continue; + } + reachable.add(file); + + for (const match of readFileSync(file, "utf8").matchAll(relativeImportPattern)) { + pending.push(resolveImport(file, match[1])); + } +} + +function listTypeScriptFiles(directory) { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const path = resolve(directory, entry.name); + return entry.isDirectory() ? listTypeScriptFiles(path) : path.endsWith(".ts") ? [path] : []; + }); +} + +const files = listTypeScriptFiles(generatedDir); +for (const file of files) { + if (!reachable.has(file)) { + rmSync(file); + } +} + +console.log(`Kept ${reachable.size} of ${files.length} generated TypeScript files.`); diff --git a/src/bin/driver-process.ts b/src/bin/driver-process.ts index ca6d799..1bb5349 100644 --- a/src/bin/driver-process.ts +++ b/src/bin/driver-process.ts @@ -22,8 +22,6 @@ import type { Logger } from "../observability"; import { summarizeDriverBootPayload } from "../observability/driver-debug"; import { DRIVER_PROTOCOL_VERSION } from "../protocol/boot"; import type { DriverBootPayload } from "../protocol/boot"; -import { createDriverHostIntegrationSnapshotFromBootExecution } from "../protocol/host-integration"; -import type { DriverHostIntegrationSnapshot } from "../protocol/host-integration"; import { parseRunId } from "../protocol/id"; import type { RunId } from "../protocol/id"; import { createDriverStartInputFromBootPayload } from "../protocol/start"; @@ -52,7 +50,6 @@ export class DriverProcess { private readonly payload: DriverBootPayload; readonly #permissionBroker: DriverPermissionBroker; readonly #shutdownController = new AbortController(); - readonly #hostSnapshot: DriverHostIntegrationSnapshot; readonly #runtimeState = new DriverRuntimeStateMachine("created"); readonly #startInput: DriverStartInput; #shutdownReason: string | null = null; @@ -67,7 +64,6 @@ export class DriverProcess { ) { this.#backendFactory = backendFactory; this.payload = payload; - this.#hostSnapshot = createDriverHostIntegrationSnapshotFromBootExecution(payload.execution); this.#startInput = createDriverStartInputFromBootPayload(payload); this.#permissionBroker = new DriverPermissionBroker(() => this.#logger); this.#heartbeatLoop = new DriverHeartbeatLoop({ @@ -531,9 +527,6 @@ export class DriverProcess { prepare: (command, signal) => prepareRemoteHttpMcpCommand(this.#startInput, command, signal), }, - hostIntegration: { - snapshot: async () => this.#hostSnapshot, - }, skill: { materialize: async (execution, signal) => materializeResolvedSkills(execution, logger, signal), diff --git a/src/contract/common.ts b/src/contract/common.ts index 8b44876..e2e4f44 100644 --- a/src/contract/common.ts +++ b/src/contract/common.ts @@ -5,7 +5,10 @@ export const protocolVersionSchema = z.literal(PROTOCOL_VERSION); export const timestampSchema = z.iso.datetime({ offset: true }); export const revisionSchema = z.number().int().nonnegative().safe(); -export const protocolIdSchema = z.ulid().transform((value) => value.toUpperCase()); +export const protocolIdSchema = z + .ulid() + .refine((value) => /^[0-7]/u.test(value), "ULID timestamp exceeds its 128-bit range") + .transform((value) => value.toUpperCase()); export const opaqueIdSchema = z.string().min(1).max(256); export const sha256Schema = z.string().regex(/^sha256:[0-9a-f]{64}$/u); export const requestDigestSchema = z.strictObject({ diff --git a/src/core/agent-driver-backend.ts b/src/core/agent-driver-backend.ts index 77bbe83..09df56e 100644 --- a/src/core/agent-driver-backend.ts +++ b/src/core/agent-driver-backend.ts @@ -2,7 +2,6 @@ import type { AgentDriverCommandSource, AgentDriverEventSink, AgentDriverFilePort, - AgentDriverHostIntegrationPort, AgentDriverHostPorts, AgentDriverMcpPort, AgentDriverPermissionPort, @@ -32,7 +31,6 @@ export type AgentDriverContextPortOverrides = Partial<{ commandSource: AgentDriverCommandSource; eventSink: AgentDriverEventSink; file: AgentDriverFilePort; - hostIntegration: AgentDriverHostIntegrationPort; mcp: AgentDriverMcpPort; permission: AgentDriverPermissionPort; skill: AgentDriverSkillPort; @@ -105,9 +103,6 @@ function createDefaultHostPorts(input: AgentDriverContextInput): AgentDriverHost await pushLosslessEvents(eventSink, [event]); }, }, - hostIntegration: { - snapshot: async () => null, - }, mcp: { prepare: async () => { throw new Error("Driver MCP host port is not configured."); diff --git a/src/host-ports/index.ts b/src/host-ports/index.ts index 079291e..b7374ab 100644 --- a/src/host-ports/index.ts +++ b/src/host-ports/index.ts @@ -1,6 +1,5 @@ import type { DriverEventInput } from "../protocol/events"; import type { DriverExecutionInput } from "../protocol/execution"; -import type { DriverHostIntegrationSnapshot } from "../protocol/host-integration"; import type { RunId } from "../protocol/id"; import type { DriverEventBatchOutput } from "../protocol/orpc"; import type { @@ -103,15 +102,10 @@ export interface AgentDriverFilePort { }): Promise; } -export interface AgentDriverHostIntegrationPort { - snapshot(): Promise; -} - export interface AgentDriverHostPorts { commandSource: AgentDriverCommandSource; eventSink: AgentDriverEventSink; file: AgentDriverFilePort; - hostIntegration: AgentDriverHostIntegrationPort; mcp: AgentDriverMcpPort; permission: AgentDriverPermissionPort; skill: AgentDriverSkillPort; diff --git a/src/index.ts b/src/index.ts index 9e7956e..1fc087c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,7 +89,6 @@ export type { AgentDriverMcpPort, AgentDriverSkillPort, AgentDriverFilePort, - AgentDriverHostIntegrationPort, } from "./host-ports"; export type { AgentDriverBackend, @@ -123,7 +122,6 @@ export type { DriverExecutionRunInput, DriverExecutionSessionInput, } from "./protocol/execution"; -export type { DriverHostIntegrationSnapshot } from "./protocol/host-integration"; export type { DriverStartInput } from "./protocol/start"; export { createDriverId, diff --git a/src/infrastructure/runtime/driver-instance-socket.ts b/src/infrastructure/runtime/driver-instance-socket.ts index a5b4f98..e5b4ba6 100644 --- a/src/infrastructure/runtime/driver-instance-socket.ts +++ b/src/infrastructure/runtime/driver-instance-socket.ts @@ -7,6 +7,7 @@ import { assertDriverEventReceiptPrefix } from "../../core/driver-runtime-io"; import type { DriverBootPayload } from "../../protocol/boot"; import type { DriverEventEnvelope, DriverEventInput } from "../../protocol/events"; import type { RunId } from "../../protocol/id"; +import { driverRuntimeRpcSchemas } from "../../protocol/orpc"; import type { DriverFailureInput, DriverEventBatchOutput, @@ -27,7 +28,6 @@ import type { RuntimeCommand, RuntimeCommandResult, } from "../../runtime-command"; -import { parseRuntimeCommand } from "../../runtime-command"; import { raceWithAbort } from "../../utils/async"; import { dialDriverControlSocket } from "./driver-control-dial"; import type { DriverWireSocket } from "./driver-control-dial"; @@ -136,11 +136,11 @@ export class DriverInstanceSocket { this.#eventBatchMaxSize = null; this.#rpcAbortController = new AbortController(); this.#socket = socket; - this.#client = createORPCClient( + this.#client = createORPCClient( new RPCLink({ websocket: socket, }), - ) as unknown as DriverRuntimeClient; + ); socket.addEventListener("close", (event) => { if (generation !== this.#connectionGeneration || this.#socket !== socket) { @@ -223,15 +223,17 @@ export class DriverInstanceSocket { }, signal: AbortSignal, ): Promise { - await this.#requireClient().driver.commandUpdate( - { - commandId: input.commandId, - driverInstanceId: this.payload.driverInstanceId, - ...(input.error === undefined ? {} : { error: input.error }), - status: input.status, - ...(input.result === undefined ? {} : { result: input.result }), - }, - this.#rpcOptions(signal), + driverRuntimeRpcSchemas.driver.commandUpdate.output.parse( + await this.#requireClient().driver.commandUpdate( + { + commandId: input.commandId, + driverInstanceId: this.payload.driverInstanceId, + ...(input.error === undefined ? {} : { error: input.error }), + status: input.status, + ...(input.result === undefined ? {} : { result: input.result }), + }, + this.#rpcOptions(signal), + ), ); } @@ -240,12 +242,14 @@ export class DriverInstanceSocket { signal: AbortSignal, ): Promise { const result: DriverExternalToolEffectClaimOutput = - await this.#requireClient().driver.claimExternalToolEffect( - { - commandId: input.commandId, - driverInstanceId: this.payload.driverInstanceId, - }, - this.#rpcOptions(signal), + driverRuntimeRpcSchemas.driver.claimExternalToolEffect.output.parse( + await this.#requireClient().driver.claimExternalToolEffect( + { + commandId: input.commandId, + driverInstanceId: this.payload.driverInstanceId, + }, + this.#rpcOptions(signal), + ), ); return result; @@ -259,16 +263,18 @@ export class DriverInstanceSocket { }, signal: AbortSignal, ): Promise { - await this.#requireClient().driver.completeExternalToolEffect( - { - commandId: input.commandId, - driverInstanceId: this.payload.driverInstanceId, - ...(input.providerReceiptJson === undefined - ? {} - : { providerReceiptJson: input.providerReceiptJson }), - result: input.result, - }, - this.#effectSettlementRpcOptions(signal), + driverRuntimeRpcSchemas.driver.completeExternalToolEffect.output.parse( + await this.#requireClient().driver.completeExternalToolEffect( + { + commandId: input.commandId, + driverInstanceId: this.payload.driverInstanceId, + ...(input.providerReceiptJson === undefined + ? {} + : { providerReceiptJson: input.providerReceiptJson }), + result: input.result, + }, + this.#effectSettlementRpcOptions(signal), + ), ); } @@ -281,13 +287,15 @@ export class DriverInstanceSocket { } async heartbeat(input: Omit): Promise { - return this.#requireClient().driver.heartbeat( - { - at: input.at, - pid: process.pid, - reason: input.reason, - }, - this.#rpcOptions(), + return driverRuntimeRpcSchemas.driver.heartbeat.output.parse( + await this.#requireClient().driver.heartbeat( + { + at: input.at, + pid: process.pid, + reason: input.reason, + }, + this.#rpcOptions(), + ), ); } @@ -298,33 +306,24 @@ export class DriverInstanceSocket { ): Promise { const generation = this.#connectionGeneration; const client = this.#requireClient(); - const result = await client.driver.hello( - { - capabilities: input.capabilities, - driverVersion: input.driverVersion, - pid: process.pid, - protocolVersion: input.protocolVersion, - runtime: this.payload.runtime, - startedAt: input.startedAt, - }, - this.#rpcOptions(), + const result = driverRuntimeRpcSchemas.driver.hello.output.parse( + await client.driver.hello( + { + capabilities: input.capabilities, + driverVersion: input.driverVersion, + pid: process.pid, + protocolVersion: input.protocolVersion, + runtime: this.payload.runtime, + startedAt: input.startedAt, + }, + this.#rpcOptions(), + ), ); if (generation !== this.#connectionGeneration || client !== this.#client) { throw new Error("Driver socket connection changed during hello."); } - if (!Number.isSafeInteger(result.heartbeatIntervalMs) || result.heartbeatIntervalMs < 250) { - throw new Error("Driver heartbeat interval must be an integer of at least 250ms."); - } - - if ( - !Number.isSafeInteger(result.runConfig.eventBatchMaxSize) || - result.runConfig.eventBatchMaxSize < 1 - ) { - throw new Error("Driver event batch max size must be a positive integer."); - } - this.#eventBatchMaxSize = result.runConfig.eventBatchMaxSize; return result; } @@ -479,12 +478,14 @@ export class DriverInstanceSocket { let remaining = events.slice(index, index + maxBatchSize); while (remaining.length > 0) { - const result = await client.driver.pushEvents( - { - driverInstanceId: this.payload.driverInstanceId, - events: remaining, - }, - rpcOptions, + const result = driverRuntimeRpcSchemas.driver.pushEvents.output.parse( + await client.driver.pushEvents( + { + driverInstanceId: this.payload.driverInstanceId, + events: remaining, + }, + rpcOptions, + ), ); this.#assertConnection(client, generation, "event delivery"); this.#assertRunGeneration(prepared); @@ -516,23 +517,27 @@ export class DriverInstanceSocket { } async pushLogs(input: Omit): Promise { - await this.#requireClient().driver.pushLogs( - { - driverInstanceId: this.payload.driverInstanceId, - logs: input.logs, - }, - this.#rpcOptions(), + driverRuntimeRpcSchemas.driver.pushLogs.output.parse( + await this.#requireClient().driver.pushLogs( + { + driverInstanceId: this.payload.driverInstanceId, + logs: input.logs, + }, + this.#rpcOptions(), + ), ); } async ready(input: Omit): Promise { - await this.#requireClient().driver.ready( - { - at: input.at, - driverInstanceId: this.payload.driverInstanceId, - pid: process.pid, - }, - this.#rpcOptions(), + driverRuntimeRpcSchemas.driver.ready.output.parse( + await this.#requireClient().driver.ready( + { + at: input.at, + driverInstanceId: this.payload.driverInstanceId, + pid: process.pid, + }, + this.#rpcOptions(), + ), ); } @@ -542,11 +547,13 @@ export class DriverInstanceSocket { let result: Awaited>; try { - result = await this.#requireClient().driverInstance.nextCommand( - { - driverInstanceId: this.payload.driverInstanceId, - }, - this.#rpcOptions(signal, timeoutSignal), + result = driverRuntimeRpcSchemas.driverInstance.nextCommand.output.parse( + await this.#requireClient().driverInstance.nextCommand( + { + driverInstanceId: this.payload.driverInstanceId, + }, + this.#rpcOptions(signal, timeoutSignal), + ), ); } catch (error) { if ( @@ -561,19 +568,21 @@ export class DriverInstanceSocket { throw error; } - return result.command === null ? null : parseRuntimeCommand(result.command); + return result.command; } async markExternalToolEffectUnknown( input: { commandId: string }, signal: AbortSignal, ): Promise { - await this.#requireClient().driver.markExternalToolEffectUnknown( - { - commandId: input.commandId, - driverInstanceId: this.payload.driverInstanceId, - }, - this.#effectSettlementRpcOptions(signal), + driverRuntimeRpcSchemas.driver.markExternalToolEffectUnknown.output.parse( + await this.#requireClient().driver.markExternalToolEffectUnknown( + { + commandId: input.commandId, + driverInstanceId: this.payload.driverInstanceId, + }, + this.#effectSettlementRpcOptions(signal), + ), ); } @@ -625,17 +634,21 @@ export class DriverInstanceSocket { const options = this.#rpcOptions(signal); if (terminal.status === "completed") { - await client.driver.completeRun( - { driverInstanceId: this.payload.driverInstanceId }, - options, + driverRuntimeRpcSchemas.driver.completeRun.output.parse( + await client.driver.completeRun( + { driverInstanceId: this.payload.driverInstanceId }, + options, + ), ); } else { - await client.driver.failRun( - { - driverInstanceId: this.payload.driverInstanceId, - error: structuredClone(terminal.error), - }, - options, + driverRuntimeRpcSchemas.driver.failRun.output.parse( + await client.driver.failRun( + { + driverInstanceId: this.payload.driverInstanceId, + error: structuredClone(terminal.error), + }, + options, + ), ); } diff --git a/src/protocol/boot/host-snapshot.ts b/src/protocol/boot/host-snapshot.ts index d1588b3..5de18bf 100644 --- a/src/protocol/boot/host-snapshot.ts +++ b/src/protocol/boot/host-snapshot.ts @@ -1,4 +1,7 @@ -import type { DriverId, SessionId, RunId } from "../id"; +import { z } from "zod"; + +import type { DriverId, RunId, SessionId } from "../id"; +import { DRIVER_ID_INPUT_PATTERN, normalizeDriverId } from "../id"; import type { AccountId, AgentDeploymentVersionId, @@ -8,108 +11,76 @@ import type { SandboxId, SandboxSessionId, } from "./host-ids"; -import { parseId, parseNullableId, readNonEmptyString, readNumber, readRecord } from "./readers"; -export interface DriverOrigin { - readonly callerUserId: AccountId; - readonly entrypoint: "api" | "chat"; - readonly executionOwnerUserId: AccountId; - readonly type: "agent"; -} +const driverIdInputPattern = new RegExp(DRIVER_ID_INPUT_PATTERN, "u"); +const nonEmptyStringSchema = z.string().min(1); -export interface DriverExecutionSessionContext { - readonly homePath: string; - readonly origin: DriverOrigin; - readonly sandboxId: SandboxId; - readonly sandboxKind: string; - readonly sandboxSessionId: SandboxSessionId; - readonly sandboxSubjectId: DriverId; - readonly sandboxSubjectKind: string; - readonly sessionOrganizationPath: string; +export function ownObjectSchema(shape: Shape) { + const keys = Object.keys(shape); + return z.preprocess( + (value) => + value !== null && typeof value === "object" && !Array.isArray(value) + ? Object.fromEntries( + keys.flatMap((key) => + Object.hasOwn(value, key) ? [[key, (value as Record)[key]]] : [], + ), + ) + : value, + z.object(shape), + ); } -export interface DriverConfigRevision { - readonly agentId: AgentId; - readonly deploymentVersionId: AgentDeploymentVersionId | null; - readonly deploymentVersionNumber: number | null; - readonly environmentId: EnvironmentId; - readonly environmentRevisionId: EnvironmentRevisionId; - readonly runId: RunId | null; - readonly sessionId: SessionId; -} +export function ownArraySchema(element: Element) { + return z.preprocess((value) => { + if (!Array.isArray(value)) { + return value; + } -function readOrigin(value: unknown): DriverOrigin { - const record = readRecord(value, "execution.session.context.origin"); - const entrypoint = readNonEmptyString(record, "entrypoint", "execution.session.context.origin"); - const type = readNonEmptyString(record, "type", "execution.session.context.origin"); + const copy = Array.from({ length: value.length }); + for (let index = 0; index < value.length; index += 1) { + copy[index] = Object.hasOwn(value, index) ? value[index] : undefined; + } + return copy; + }, z.array(element)); +} - if (entrypoint !== "api" && entrypoint !== "chat") { - throw new TypeError("execution.session.context.origin.entrypoint must be api or chat."); - } +export function createDriverIdSchema() { + return z + .string() + .regex(driverIdInputPattern, "must be a valid ULID") + .transform((value) => normalizeDriverId(value) as Id); +} - if (type !== "agent") { - throw new TypeError("execution.session.context.origin.type must be agent."); - } +export const driverOriginSchema = ownObjectSchema({ + callerUserId: createDriverIdSchema(), + entrypoint: z.enum(["api", "chat"]), + executionOwnerUserId: createDriverIdSchema(), + type: z.literal("agent"), +}); - return { - callerUserId: parseId(record["callerUserId"], "Driver origin caller user ID") as AccountId, - entrypoint, - executionOwnerUserId: parseId( - record["executionOwnerUserId"], - "Driver origin execution owner user ID", - ) as AccountId, - type, - }; -} +export type DriverOrigin = z.infer; -export function readConfigRevision(value: unknown): DriverConfigRevision { - const record = readRecord(value, "execution.configRevision"); +export const driverExecutionSessionContextSchema = ownObjectSchema({ + homePath: nonEmptyStringSchema, + origin: driverOriginSchema, + sandboxId: createDriverIdSchema(), + sandboxKind: nonEmptyStringSchema, + sandboxSessionId: createDriverIdSchema(), + sandboxSubjectId: createDriverIdSchema(), + sandboxSubjectKind: nonEmptyStringSchema, + sessionOrganizationPath: nonEmptyStringSchema, +}); - return { - agentId: parseId(record["agentId"], "Driver config agent ID") as AgentId, - deploymentVersionId: parseNullableId( - record["deploymentVersionId"], - "Driver config deployment version ID", - ) as AgentDeploymentVersionId | null, - deploymentVersionNumber: - record["deploymentVersionNumber"] === null - ? null - : readNumber(record, "deploymentVersionNumber", "execution.configRevision"), - environmentId: parseId( - record["environmentId"], - "Driver config environment ID", - ) as EnvironmentId, - environmentRevisionId: parseId( - record["environmentRevisionId"], - "Driver config environment revision ID", - ) as EnvironmentRevisionId, - runId: parseNullableId(record["runId"], "Driver config run ID") as RunId | null, - sessionId: parseId(record["sessionId"], "Driver config session ID") as SessionId, - }; -} +export type DriverExecutionSessionContext = z.infer; -export function readExecutionSessionContext(value: unknown): DriverExecutionSessionContext { - const record = readRecord(value, "execution.session.context"); +export const driverConfigRevisionSchema = ownObjectSchema({ + agentId: createDriverIdSchema(), + deploymentVersionId: createDriverIdSchema().nullable(), + deploymentVersionNumber: z.number().finite().nullable(), + environmentId: createDriverIdSchema(), + environmentRevisionId: createDriverIdSchema(), + runId: createDriverIdSchema().nullable(), + sessionId: createDriverIdSchema(), +}); - return { - homePath: readNonEmptyString(record, "homePath", "execution.session.context"), - origin: readOrigin(record["origin"]), - sandboxId: parseId(record["sandboxId"], "Driver execution sandbox ID") as SandboxId, - sandboxKind: readNonEmptyString(record, "sandboxKind", "execution.session.context"), - sandboxSessionId: parseId( - record["sandboxSessionId"], - "Driver execution sandbox session ID", - ) as SandboxSessionId, - sandboxSubjectId: parseId(record["sandboxSubjectId"], "Driver execution sandbox subject ID"), - sandboxSubjectKind: readNonEmptyString( - record, - "sandboxSubjectKind", - "execution.session.context", - ), - sessionOrganizationPath: readNonEmptyString( - record, - "sessionOrganizationPath", - "execution.session.context", - ), - }; -} +export type DriverConfigRevision = z.infer; diff --git a/src/protocol/boot/index.ts b/src/protocol/boot/index.ts index 870dfe3..7b091f2 100644 --- a/src/protocol/boot/index.ts +++ b/src/protocol/boot/index.ts @@ -1,30 +1,26 @@ import { delimiter, isAbsolute } from "node:path"; +import { z } from "zod"; import type { DriverInstanceId } from "../id"; import type { JsonObject } from "../json"; import { readJsonObject } from "../json"; -import type { DriverNativeRuntimeRef, DriverRuntime, DriverRuntimeTransport } from "../runtime"; +import type { DriverNativeRuntimeRef } from "../runtime"; import { isSupportedDriverRuntime, isSupportedDriverRuntimeTransport, + parseDriverNativeRuntimeRef, SUPPORTED_DRIVER_NATIVE_RUNTIME_REF_KINDS, SUPPORTED_DRIVER_RUNTIMES, SUPPORTED_DRIVER_RUNTIME_TRANSPORTS, } from "../runtime"; import type { CredentialId, McpServerId, SandboxId, SkillId, SkillSnapshotId } from "./host-ids"; -import type { DriverConfigRevision, DriverExecutionSessionContext } from "./host-snapshot"; -import { readConfigRevision, readExecutionSessionContext } from "./host-snapshot"; import { - parseId, - readArray, - readInteger, - readNonEmptyString, - readNumber, - readOptionalNullableString, - readRecord, - readString, - readStringArray, -} from "./readers"; + createDriverIdSchema, + driverConfigRevisionSchema, + driverExecutionSessionContextSchema, + ownArraySchema, + ownObjectSchema, +} from "./host-snapshot"; export type { AccountId, @@ -70,104 +66,191 @@ export type { DriverRuntimeTransport, } from "../runtime"; -export interface DriverExecutionEnvironment { - readonly paths?: { - readonly executable: string[]; - readonly node: string[]; - readonly python: string[]; - }; - readonly variables: Record; -} - -export type DriverBuiltInToolName = - | "bash" - | "edit" - | "glob" - | "grep" - | "read" - | "web_fetch" - | "web_search" - | "write"; - -export interface DriverBuiltInToolConfig { - readonly enabled: boolean; - readonly name: DriverBuiltInToolName; +const nonEmptyStringSchema = z.string().min(1); +const nullableOptionalStringSchema = z.string().nullable().optional(); +const resolutionModeSchema = z.enum(["auto", "explicit", "tombstone"]); +const runtimeTransportByRuntime = { + "acp-fallback": "acp-fallback", + "claude-agent-sdk": "claude-agent-sdk", + "openai-runtime": "openai-app-server", +} as const; +const controlUrlSchema = z + .url() + .refine((value) => /^(?:https?|wss?):/iu.test(value), "must use http, https, ws, or wss"); + +function omitUndefinedProperties>(value: Value): Value { + return Object.fromEntries( + Object.entries(value).filter(([, entry]) => entry !== undefined), + ) as Value; } -function readBoolean(value: unknown, label: string): boolean { - if (typeof value !== "boolean") { - throw new TypeError(`${label} must be a boolean.`); +const absolutePathSchema = nonEmptyStringSchema.refine( + (path) => isAbsolute(path) && !path.includes("\0") && !path.includes(delimiter), + "must be an absolute path without null bytes or path delimiters", +); + +const environmentEntrySchema = z.tuple([ + z + .string() + .refine( + (name) => name.length > 0 && !name.includes("=") && !name.includes("\0"), + "must be a valid environment entry", + ), + z.string().refine((value) => !value.includes("\0"), "must be a valid environment entry"), +]); + +const environmentVariablesSchema = z + .unknown() + .refine( + (value) => typeof value === "object" && value !== null && !Array.isArray(value), + "must be an object", + ) + .transform((value) => Object.entries(value as Record)) + .pipe(z.array(environmentEntrySchema)) + .transform((entries) => Object.fromEntries(entries)); + +const jsonObjectSchema = z.unknown().transform((value, context): JsonObject => { + try { + return readJsonObject(value, "execution.providerOptions"); + } catch (error) { + context.addIssue({ + code: "custom", + message: error instanceof Error ? error.message : "must be a JSON object", + }); + return z.NEVER; } +}); + +const driverExecutionEnvironmentSchema = ownObjectSchema({ + paths: ownObjectSchema({ + executable: ownArraySchema(absolutePathSchema), + node: ownArraySchema(absolutePathSchema), + python: ownArraySchema(absolutePathSchema), + }).optional(), + variables: environmentVariablesSchema, +}).transform(omitUndefinedProperties); + +export type DriverExecutionEnvironment = z.infer; + +const driverBuiltInToolNameSchema = z.enum([ + "bash", + "edit", + "glob", + "grep", + "read", + "web_fetch", + "web_search", + "write", +]); + +export type DriverBuiltInToolName = z.infer; + +const driverBuiltInToolConfigSchema = ownObjectSchema({ + enabled: z.boolean(), + name: driverBuiltInToolNameSchema, +}); + +export type DriverBuiltInToolConfig = z.infer; + +const driverSkillCatalogFrontmatterSummarySchema = ownObjectSchema({ + author: nullableOptionalStringSchema.transform((value) => value ?? null), + description: nullableOptionalStringSchema.transform((value) => value ?? null), + version: nullableOptionalStringSchema.transform((value) => value ?? null), +}); + +export type DriverSkillCatalogFrontmatterSummary = z.infer< + typeof driverSkillCatalogFrontmatterSummarySchema +>; + +const driverSkillCatalogEntrySchema = ownObjectSchema({ + frontmatter: driverSkillCatalogFrontmatterSummarySchema, + mountPath: nonEmptyStringSchema, + resolutionMode: resolutionModeSchema, + skillId: createDriverIdSchema(), + skillName: nonEmptyStringSchema, +}); + +export type DriverSkillCatalogEntry = z.infer; + +const driverResolvedSkillSchema = ownObjectSchema({ + archiveFormat: z.literal("zip"), + blobSha256: nonEmptyStringSchema, + compression: z.literal("deflate"), + downloadUrl: nonEmptyStringSchema, + materializationStatus: z.enum(["failed", "pending", "ready", "skipped"]), + mountPath: nonEmptyStringSchema, + resolutionMode: resolutionModeSchema, + skillId: createDriverIdSchema(), + skillName: nonEmptyStringSchema, + snapshotId: createDriverIdSchema().nullable().optional(), + warningCode: nullableOptionalStringSchema, +}).transform(omitUndefinedProperties); + +export type DriverResolvedSkill = z.infer; + +const bootMcpServerCommonShape = { + authType: nonEmptyStringSchema, + credentialScope: nonEmptyStringSchema, + credentialStatus: nonEmptyStringSchema, + name: nonEmptyStringSchema, + serverId: createDriverIdSchema(), + subjectLabel: nullableOptionalStringSchema, +}; - return value; -} - -export interface DriverSkillCatalogFrontmatterSummary { - readonly author: string | null; - readonly description: string | null; - readonly version: string | null; -} - -export interface DriverSkillCatalogEntry { - readonly frontmatter: DriverSkillCatalogFrontmatterSummary; - readonly mountPath: string; - readonly resolutionMode: "auto" | "explicit" | "tombstone"; - readonly skillId: SkillId; - readonly skillName: string; -} - -export interface DriverResolvedSkill { - readonly archiveFormat: "zip"; - readonly blobSha256: string; - readonly compression: "deflate"; - readonly downloadUrl: string; - readonly materializationStatus: "failed" | "pending" | "ready" | "skipped"; - readonly mountPath: string; - readonly resolutionMode: "auto" | "explicit" | "tombstone"; - readonly skillId: SkillId; - readonly skillName: string; - readonly snapshotId?: SkillSnapshotId | null | undefined; - readonly warningCode?: string | null | undefined; -} - -export interface AuthorizedDriverBootMcpServer { - readonly authType: string; - readonly authorizationState: "active"; - readonly credentialId: CredentialId; - readonly credentialScope: string; - readonly credentialStatus: string; - readonly name: string; - readonly proxyGrantId: string; - readonly proxyUrl: string; - readonly serverId: McpServerId; - readonly subjectLabel?: string | null | undefined; -} - -export interface UnavailableDriverBootMcpServer { - readonly authType: string; - readonly authorizationState: "authorization_required" | "disabled" | "expired" | "revoked"; - readonly credentialScope: string; - readonly credentialStatus: string; - readonly name: string; - readonly serverId: McpServerId; - readonly subjectLabel?: string | null | undefined; -} - -export type DriverBootMcpServer = AuthorizedDriverBootMcpServer | UnavailableDriverBootMcpServer; +const authorizedDriverBootMcpServerSchema = ownObjectSchema({ + ...bootMcpServerCommonShape, + authorizationState: z.literal("active"), + credentialId: createDriverIdSchema(), + proxyGrantId: nonEmptyStringSchema, + proxyUrl: nonEmptyStringSchema, +}); + +export type AuthorizedDriverBootMcpServer = z.infer; + +const unavailableDriverBootMcpServerSchema = ownObjectSchema({ + ...bootMcpServerCommonShape, + authorizationState: z.enum(["authorization_required", "disabled", "expired", "revoked"]), +}); + +export type UnavailableDriverBootMcpServer = z.infer; + +const driverBootMcpServerSchema = z + .union([authorizedDriverBootMcpServerSchema, unavailableDriverBootMcpServerSchema]) + .transform(omitUndefinedProperties); + +export type DriverBootMcpServer = z.infer; + +const driverRecoveryMessageSchema = ownObjectSchema({ + content: nonEmptyStringSchema, + role: z.enum(["assistant", "user"]), +}); + +export type DriverRecoveryMessage = z.infer; + +const driverNativeRuntimeRefSchema = z + .unknown() + .transform((value, context): DriverNativeRuntimeRef => { + try { + return parseDriverNativeRuntimeRef(value); + } catch (error) { + context.addIssue({ + code: "custom", + message: error instanceof Error ? error.message : "must be a valid native runtime ref", + }); + return z.NEVER; + } + }); -export interface DriverRecoveryMessage { - readonly content: string; - readonly role: "assistant" | "user"; -} +const driverExecutionSessionSpecSchema = ownObjectSchema({ + additionalDirectories: ownArraySchema(nonEmptyStringSchema), + context: driverExecutionSessionContextSchema, + cwd: nonEmptyStringSchema, + mcpServers: ownArraySchema(driverBootMcpServerSchema), + nativeResumeRef: driverNativeRuntimeRefSchema.nullable(), + recoveryMessages: ownArraySchema(driverRecoveryMessageSchema).default([]), +}); -export interface DriverExecutionSessionSpec { - readonly additionalDirectories: string[]; - readonly context: DriverExecutionSessionContext; - readonly cwd: string; - readonly mcpServers: DriverBootMcpServer[]; - readonly nativeResumeRef: DriverNativeRuntimeRef | null; - readonly recoveryMessages: DriverRecoveryMessage[]; -} +export type DriverExecutionSessionSpec = z.infer; /** * How the driver mediates tool-permission requests. @@ -179,390 +262,83 @@ export interface DriverExecutionSessionSpec { * the control plane for an interactive allow/deny decision. Providers may * still auto-approve actions they classify as trusted or read-only. */ -export type DriverPermissionPolicy = "full_access" | "supervised"; - -export const DEFAULT_DRIVER_PERMISSION_POLICY = "full_access" satisfies DriverPermissionPolicy; - -export interface DriverExecutionSpec { - readonly builtInTools: DriverBuiltInToolConfig[]; - readonly configRevision: DriverConfigRevision; - readonly environment: DriverExecutionEnvironment; - readonly model: string; - readonly permissionPolicy: DriverPermissionPolicy; - readonly profilePrompt: string; - readonly provider: string; - readonly providerOptions: JsonObject; - readonly session: DriverExecutionSessionSpec; - readonly skillCatalog: DriverSkillCatalogEntry[]; - readonly skills: DriverResolvedSkill[]; -} - -export interface DriverBootPayload { - readonly bootToken: string; - readonly controlUrl: string; - readonly driverControlPort: number; - readonly driverGeneration: number; - readonly driverInstanceId: DriverInstanceId; - readonly execution: DriverExecutionSpec; - readonly heartbeatIntervalMs: number; - readonly protocolVersion: typeof DRIVER_PROTOCOL_VERSION; - readonly runtime: DriverRuntime; - readonly runtimeTransport: DriverRuntimeTransport; - readonly sandboxId: SandboxId; - readonly traceparent: string; -} +const driverPermissionPolicySchema = z + .enum(["full_access", "supervised"]) + .nullish() + .transform((value) => value ?? "full_access"); -function readVariables(value: unknown): Record { - const record = readRecord(value, "execution.environment.variables"); - const entries = Object.entries(record); - - for (const [key, entry] of entries) { - if ( - key.length === 0 || - key.includes("=") || - key.includes("\0") || - typeof entry !== "string" || - entry.includes("\0") - ) { - throw new TypeError( - `execution.environment.variables.${key} must be a valid environment entry.`, - ); - } - } - - return Object.fromEntries(entries) as Record; -} +export type DriverPermissionPolicy = z.infer; -function readAbsolutePathArray(record: Record, field: string): string[] { - const label = "execution.environment.paths"; - return readStringArray(record, field, label).map((path, index) => { - if (!isAbsolute(path) || path.includes("\0") || path.includes(delimiter)) { - throw new TypeError( - `${label}.${field}[${index}] must be an absolute path without null bytes or path delimiters.`, - ); - } - - return path; - }); -} - -function readEnvironmentPaths(value: unknown): NonNullable { - const paths = readRecord(value, "execution.environment.paths"); - return { - executable: readAbsolutePathArray(paths, "executable"), - node: readAbsolutePathArray(paths, "node"), - python: readAbsolutePathArray(paths, "python"), - }; -} - -function readNativeRuntimeRef(value: unknown): DriverNativeRuntimeRef | null { - if (value === null) { - return null; - } - - const record = readRecord(value, "execution.session.nativeResumeRef"); - const kind = readNonEmptyString(record, "kind", "execution.session.nativeResumeRef"); - const runtimeId = readNonEmptyString(record, "runtimeId", "execution.session.nativeResumeRef"); - - if (kind !== "openai_thread_id" && kind !== "claude_session_id" && kind !== "acp_session_id") { - throw new TypeError("execution.session.nativeResumeRef.kind is unsupported."); - } - - if (!isSupportedDriverRuntime(runtimeId)) { - throw new TypeError("execution.session.nativeResumeRef.runtimeId is unsupported."); - } - - return { - kind, - runtimeId, - value: readNonEmptyString(record, "value", "execution.session.nativeResumeRef"), - }; -} - -function readSkillFrontmatter(value: unknown, label: string): DriverSkillCatalogFrontmatterSummary { - const record = readRecord(value, `${label}.frontmatter`); - - return { - author: readOptionalNullableString(record, "author", `${label}.frontmatter`) ?? null, - description: readOptionalNullableString(record, "description", `${label}.frontmatter`) ?? null, - version: readOptionalNullableString(record, "version", `${label}.frontmatter`) ?? null, - }; -} - -function readResolutionMode(value: unknown, label: string): DriverResolvedSkill["resolutionMode"] { - if (value === "auto" || value === "explicit" || value === "tombstone") { - return value; - } - - throw new TypeError(`${label}.resolutionMode is unsupported.`); -} - -function readSkillCatalogEntry(value: unknown, index: number): DriverSkillCatalogEntry { - const label = `execution.skillCatalog[${index}]`; - const record = readRecord(value, label); - - return { - frontmatter: readSkillFrontmatter(record["frontmatter"], label), - mountPath: readNonEmptyString(record, "mountPath", label), - resolutionMode: readResolutionMode(record["resolutionMode"], label), - skillId: parseId(record["skillId"], `${label}.skillId`) as SkillId, - skillName: readNonEmptyString(record, "skillName", label), - }; -} - -function readResolvedSkill(value: unknown, index: number): DriverResolvedSkill { - const label = `execution.skills[${index}]`; - const record = readRecord(value, label); - const archiveFormat = readNonEmptyString(record, "archiveFormat", label); - const compression = readNonEmptyString(record, "compression", label); - const materializationStatus = readNonEmptyString(record, "materializationStatus", label); - const snapshotId = readOptionalNullableString(record, "snapshotId", label); - const warningCode = readOptionalNullableString(record, "warningCode", label); - - if (archiveFormat !== "zip") { - throw new TypeError(`${label}.archiveFormat must be zip.`); - } - - if (compression !== "deflate") { - throw new TypeError(`${label}.compression must be deflate.`); - } - - if ( - materializationStatus !== "failed" && - materializationStatus !== "pending" && - materializationStatus !== "ready" && - materializationStatus !== "skipped" - ) { - throw new TypeError(`${label}.materializationStatus is unsupported.`); - } - - return { - archiveFormat, - blobSha256: readNonEmptyString(record, "blobSha256", label), - compression, - downloadUrl: readNonEmptyString(record, "downloadUrl", label), - materializationStatus, - mountPath: readNonEmptyString(record, "mountPath", label), - resolutionMode: readResolutionMode(record["resolutionMode"], label), - skillId: parseId(record["skillId"], `${label}.skillId`) as SkillId, - skillName: readNonEmptyString(record, "skillName", label), - ...(snapshotId === undefined - ? {} - : { - snapshotId: - snapshotId === null - ? null - : (parseId(snapshotId, `${label}.snapshotId`) as SkillSnapshotId), - }), - ...(warningCode === undefined ? {} : { warningCode }), - }; -} +export const DEFAULT_DRIVER_PERMISSION_POLICY = "full_access" satisfies DriverPermissionPolicy; -function readBuiltInToolName(value: unknown, label: string): DriverBuiltInToolName { - if ( - value === "bash" || - value === "edit" || - value === "glob" || - value === "grep" || - value === "read" || - value === "web_fetch" || - value === "web_search" || - value === "write" - ) { - return value; +const driverExecutionSpecSchema = ownObjectSchema({ + builtInTools: ownArraySchema(driverBuiltInToolConfigSchema), + configRevision: driverConfigRevisionSchema, + environment: driverExecutionEnvironmentSchema, + model: nonEmptyStringSchema, + permissionPolicy: driverPermissionPolicySchema, + profilePrompt: z.string(), + provider: nonEmptyStringSchema, + providerOptions: jsonObjectSchema.default({}), + session: driverExecutionSessionSpecSchema, + skillCatalog: ownArraySchema(driverSkillCatalogEntrySchema), + skills: ownArraySchema(driverResolvedSkillSchema), +}); + +export type DriverExecutionSpec = z.infer; + +const driverBootPayloadSchema = ownObjectSchema({ + bootToken: nonEmptyStringSchema, + controlUrl: controlUrlSchema, + driverControlPort: z.number().int().min(DRIVER_CONTROL_PORT_MIN).max(DRIVER_CONTROL_PORT_MAX), + driverGeneration: z.number().int().nonnegative(), + driverInstanceId: createDriverIdSchema(), + execution: driverExecutionSpecSchema, + heartbeatIntervalMs: z.number().finite().min(250), + protocolVersion: z.literal(DRIVER_PROTOCOL_VERSION, { + error: `protocolVersion must be ${DRIVER_PROTOCOL_VERSION}`, + }), + runtime: z.enum(SUPPORTED_DRIVER_RUNTIMES), + runtimeTransport: z.enum(SUPPORTED_DRIVER_RUNTIME_TRANSPORTS), + sandboxId: createDriverIdSchema(), + traceparent: nonEmptyStringSchema, +}).superRefine((payload, context) => { + if (payload.sandboxId !== payload.execution.session.context.sandboxId) { + context.addIssue({ + code: "custom", + message: "Driver boot payload sandbox IDs must match", + path: ["sandboxId"], + }); } - throw new TypeError(`${label} is unsupported.`); -} - -function readBuiltInTool(value: unknown, index: number): DriverBuiltInToolConfig { - const label = `execution.builtInTools[${index}]`; - const record = readRecord(value, label); - - return { - enabled: readBoolean(record["enabled"], `${label}.enabled`), - name: readBuiltInToolName(record["name"], `${label}.name`), - }; -} - -function readBootMcpServer(value: unknown, index: number): DriverBootMcpServer { - const label = `execution.session.mcpServers[${index}]`; - const record = readRecord(value, label); - const authorizationState = readNonEmptyString(record, "authorizationState", label); - const subjectLabel = readOptionalNullableString(record, "subjectLabel", label); - const common = { - authType: readNonEmptyString(record, "authType", label), - credentialScope: readNonEmptyString(record, "credentialScope", label), - credentialStatus: readNonEmptyString(record, "credentialStatus", label), - name: readNonEmptyString(record, "name", label), - serverId: parseId(record["serverId"], `${label}.serverId`) as McpServerId, - ...(subjectLabel === undefined ? {} : { subjectLabel }), - }; - - if (authorizationState === "active") { - return { - ...common, - authorizationState, - credentialId: parseId(record["credentialId"], `${label}.credentialId`) as CredentialId, - proxyGrantId: readNonEmptyString(record, "proxyGrantId", label), - proxyUrl: readNonEmptyString(record, "proxyUrl", label), - }; + if (payload.runtimeTransport !== runtimeTransportByRuntime[payload.runtime]) { + context.addIssue({ + code: "custom", + message: `runtime ${payload.runtime} does not match transport ${payload.runtimeTransport}`, + path: ["runtimeTransport"], + }); } - if ( - authorizationState !== "authorization_required" && - authorizationState !== "disabled" && - authorizationState !== "expired" && - authorizationState !== "revoked" - ) { - throw new TypeError(`${label}.authorizationState is unsupported.`); + const nativeResumeRef = payload.execution.session.nativeResumeRef; + if (nativeResumeRef !== null && nativeResumeRef.runtimeId !== payload.runtime) { + context.addIssue({ + code: "custom", + message: `native resume runtime ${nativeResumeRef.runtimeId} does not match runtime ${payload.runtime}`, + path: ["execution", "session", "nativeResumeRef", "runtimeId"], + }); } +}); - return { - ...common, - authorizationState, - }; -} - -function readExecutionSession(value: unknown): DriverExecutionSessionSpec { - const record = readRecord(value, "execution.session"); - const recoveryMessages = - record["recoveryMessages"] === undefined - ? [] - : readArray(record["recoveryMessages"], "execution.session.recoveryMessages").map( - (entry, index): DriverRecoveryMessage => { - const label = `execution.session.recoveryMessages[${index}]`; - const message = readRecord(entry, label); - const role = readNonEmptyString(message, "role", label); - - if (role !== "assistant" && role !== "user") { - throw new TypeError(`${label}.role is unsupported.`); - } - - return { - content: readNonEmptyString(message, "content", label), - role, - }; - }, - ); - - return { - additionalDirectories: readStringArray(record, "additionalDirectories", "execution.session"), - context: readExecutionSessionContext(record["context"]), - cwd: readNonEmptyString(record, "cwd", "execution.session"), - mcpServers: readArray(record["mcpServers"], "execution.session.mcpServers").map( - readBootMcpServer, - ), - nativeResumeRef: readNativeRuntimeRef(record["nativeResumeRef"]), - recoveryMessages, - }; -} - -function readPermissionPolicy(value: unknown): DriverPermissionPolicy { - if (value === undefined || value === null) { - return DEFAULT_DRIVER_PERMISSION_POLICY; - } - - if (value === "full_access" || value === "supervised") { - return value; - } - - throw new TypeError("execution.permissionPolicy is unsupported."); -} - -function readExecution(value: unknown): DriverExecutionSpec { - const record = readRecord(value, "execution"); - const environment = readRecord(record["environment"], "execution.environment"); - - return { - builtInTools: readArray(record["builtInTools"], "execution.builtInTools").map(readBuiltInTool), - configRevision: readConfigRevision(record["configRevision"]), - environment: { - ...(environment["paths"] === undefined - ? {} - : { paths: readEnvironmentPaths(environment["paths"]) }), - variables: readVariables(environment["variables"]), - }, - model: readNonEmptyString(record, "model", "execution"), - permissionPolicy: readPermissionPolicy(record["permissionPolicy"]), - profilePrompt: readString(record, "profilePrompt", "execution"), - provider: readNonEmptyString(record, "provider", "execution"), - providerOptions: - record["providerOptions"] === undefined - ? {} - : readJsonObject(record["providerOptions"], "execution.providerOptions"), - session: readExecutionSession(record["session"]), - skillCatalog: readArray(record["skillCatalog"], "execution.skillCatalog").map( - readSkillCatalogEntry, - ), - skills: readArray(record["skills"], "execution.skills").map(readResolvedSkill), - }; -} +export type DriverBootPayload = z.infer; export function parseDriverBootPayload(value: unknown): DriverBootPayload { - const record = readRecord(value, "Driver boot payload"); - const driverControlPort = readInteger(record, "driverControlPort", "Driver boot payload"); - const driverGeneration = readInteger(record, "driverGeneration", "Driver boot payload"); - const heartbeatIntervalMs = readNumber(record, "heartbeatIntervalMs", "Driver boot payload"); - const protocolVersion = readInteger(record, "protocolVersion", "Driver boot payload"); - const runtime = readNonEmptyString(record, "runtime", "Driver boot payload"); - const runtimeTransport = readNonEmptyString(record, "runtimeTransport", "Driver boot payload"); - - if (driverControlPort < DRIVER_CONTROL_PORT_MIN || driverControlPort > DRIVER_CONTROL_PORT_MAX) { - throw new TypeError( - `Driver boot payload.driverControlPort must be between ${DRIVER_CONTROL_PORT_MIN} and ${DRIVER_CONTROL_PORT_MAX}.`, - ); - } - - if (driverGeneration < 0) { - throw new TypeError("Driver boot payload.driverGeneration must be non-negative."); - } - - if (heartbeatIntervalMs < 250) { - throw new TypeError("Driver boot payload.heartbeatIntervalMs must be at least 250."); - } - - if (protocolVersion !== DRIVER_PROTOCOL_VERSION) { - throw new TypeError(`Driver boot payload.protocolVersion must be ${DRIVER_PROTOCOL_VERSION}.`); - } - - if (!isSupportedDriverRuntime(runtime)) { - throw new TypeError(`Unsupported runtime: ${runtime}.`); - } - - if (!isSupportedDriverRuntimeTransport(runtimeTransport)) { - throw new TypeError(`Unsupported runtime transport: ${runtimeTransport}.`); - } - - const controlUrl = readNonEmptyString(record, "controlUrl", "Driver boot payload"); - - try { - void new URL(controlUrl); - } catch { - throw new TypeError("Driver boot payload.controlUrl must be an absolute URL."); - } - - const execution = readExecution(record["execution"]); - const sandboxId = parseId(record["sandboxId"], "Driver sandbox ID") as SandboxId; + const result = driverBootPayloadSchema.safeParse(value); - if (sandboxId !== execution.session.context.sandboxId) { - throw new TypeError("Driver boot payload sandbox IDs must match."); + if (!result.success) { + throw new TypeError(z.prettifyError(result.error)); } - return { - bootToken: readNonEmptyString(record, "bootToken", "Driver boot payload"), - controlUrl, - driverControlPort, - driverGeneration, - driverInstanceId: parseId(record["driverInstanceId"], "Driver instance ID") as DriverInstanceId, - execution, - heartbeatIntervalMs, - protocolVersion, - runtime, - runtimeTransport, - sandboxId, - traceparent: readNonEmptyString(record, "traceparent", "Driver boot payload"), - }; + return result.data; } export function parseDriverBootPayloadJson(raw: string): DriverBootPayload { diff --git a/src/protocol/boot/readers.ts b/src/protocol/boot/readers.ts deleted file mode 100644 index 3df3d8a..0000000 --- a/src/protocol/boot/readers.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { DriverId } from "../id"; -import { parseDriverId } from "../id"; - -export function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -export function readRecord(value: unknown, label: string): Record { - if (!isRecord(value)) { - throw new TypeError(`${label} must be an object.`); - } - - return value; -} - -export function readString(record: Record, field: string, label: string): string { - const value = record[field]; - - if (typeof value !== "string") { - throw new TypeError(`${label}.${field} must be a string.`); - } - - return value; -} - -export function readNonEmptyString( - record: Record, - field: string, - label: string, -): string { - const value = readString(record, field, label); - - if (value.length === 0) { - throw new TypeError(`${label}.${field} must be non-empty.`); - } - - return value; -} - -export function readOptionalNullableString( - record: Record, - field: string, - label: string, -): string | null | undefined { - const value = record[field]; - - if (value === undefined || value === null) { - return value; - } - - if (typeof value !== "string") { - throw new TypeError(`${label}.${field} must be a string, null, or undefined.`); - } - - return value; -} - -export function readNumber(record: Record, field: string, label: string): number { - const value = record[field]; - - if (typeof value !== "number" || !Number.isFinite(value)) { - throw new TypeError(`${label}.${field} must be a finite number.`); - } - - return value; -} - -export function readBoolean( - record: Record, - field: string, - label: string, -): boolean { - const value = record[field]; - - if (typeof value !== "boolean") { - throw new TypeError(`${label}.${field} must be a boolean.`); - } - - return value; -} - -export function readInteger(record: Record, field: string, label: string): number { - const value = readNumber(record, field, label); - - if (!Number.isInteger(value)) { - throw new TypeError(`${label}.${field} must be an integer.`); - } - - return value; -} - -export function readArray(value: unknown, label: string): unknown[] { - if (!Array.isArray(value)) { - throw new TypeError(`${label} must be an array.`); - } - - return value; -} - -export function readStringArray( - record: Record, - field: string, - label: string, -): string[] { - return readArray(record[field], `${label}.${field}`).map((entry, index) => { - if (typeof entry !== "string" || entry.length === 0) { - throw new TypeError(`${label}.${field}[${index}] must be a non-empty string.`); - } - - return entry; - }); -} - -export function parseId(value: unknown, label: string): DriverId { - return parseDriverId(value, label); -} - -export function parseNullableId(value: unknown, label: string): DriverId | null { - return value === null ? null : parseId(value, label); -} diff --git a/src/protocol/events/runtime-event-types.ts b/src/protocol/events/runtime-event-types.ts index cb18098..c819e38 100644 --- a/src/protocol/events/runtime-event-types.ts +++ b/src/protocol/events/runtime-event-types.ts @@ -6,6 +6,7 @@ export const RUNTIME_EVENT_KINDS = [ "account.limits.updated", "account.updated", "agent.task.updated", + "agent.tasks.replaced", "auth.methods.updated", "auth.session.updated", "catalog.updated", diff --git a/src/protocol/events/runtime-events.ts b/src/protocol/events/runtime-events.ts index e6f980c..7b1f234 100644 --- a/src/protocol/events/runtime-events.ts +++ b/src/protocol/events/runtime-events.ts @@ -1,4 +1,5 @@ import { jsonValueSchema } from "../../contract/common"; +import { z } from "zod"; import type { DriverInstanceId, EventId, SessionId, RunId } from "../id"; import { RUNTIME_EVENT_KINDS, @@ -50,6 +51,49 @@ import { export * from "./runtime-event-types"; const runtimeEventKindSet = new Set(RUNTIME_EVENT_KINDS); +const MAX_AGENT_TASKS_REPLACED_PAYLOAD_BYTES = 1_020 * 1_024; +const agentTasksReplacedPayloadSchema = z + .object({ + tasks: z + .array( + z + .object({ + taskId: z + .string() + .min(1) + .refine((value) => Buffer.byteLength(value, "utf8") <= 256), + taskType: z.string().min(1).max(4_096).optional(), + title: z.string().min(1).max(4_096).optional(), + }) + .strict(), + ) + .max(256) + .superRefine((tasks, context) => { + const taskIds = new Set(); + for (const [index, task] of tasks.entries()) { + if (taskIds.has(task.taskId)) { + context.addIssue({ + code: "custom", + message: "Task IDs must be unique.", + path: [index, "taskId"], + }); + } + taskIds.add(task.taskId); + } + }), + }) + .strict() + .superRefine((payload, context) => { + if ( + Buffer.byteLength(JSON.stringify(payload), "utf8") > MAX_AGENT_TASKS_REPLACED_PAYLOAD_BYTES + ) { + context.addIssue({ + code: "custom", + message: "Task snapshot exceeds the durable event capacity.", + path: ["tasks"], + }); + } + }); const runtimeEventActors = new Set(["agent", "api", "driver", "system", "tool", "user"]); const runtimeEventOrigins = new Set([ "api", @@ -197,10 +241,12 @@ export function parseRuntimeEventEnvelope(value: unknown): RuntimeEventEnvelope const payload = admitRuntimeEventPayload( { ...(driverInstanceId === undefined ? {} : { driverInstanceId }), + delivery, kind, ...(runId === undefined ? {} : { runId }), sessionId, ...(traceId === undefined ? {} : { traceId }), + visibility, }, value["payload"], ); @@ -327,11 +373,13 @@ function defaultVisibility(kind: RuntimeEventKind): RuntimeEventVisibility { function admitRuntimeEventPayload( context: { + readonly delivery: RuntimeEventDelivery; readonly driverInstanceId?: DriverInstanceId | undefined; readonly kind: RuntimeEventKind; readonly runId?: RunId | undefined; readonly sessionId: SessionId; readonly traceId?: string | undefined; + readonly visibility: RuntimeEventVisibility; }, payload: unknown, ): unknown { @@ -358,6 +406,20 @@ function admitRuntimeEventPayload( } return omitPayloadIdentity(record); } + case "agent.tasks.replaced": { + if (context.delivery !== "lossless" || context.visibility !== "participant") { + throw new Error(`Runtime event ${context.kind} must be lossless and participant-visible.`); + } + if (context.driverInstanceId === undefined || context.runId === undefined) { + throw new Error(`Runtime event ${context.kind} requires a run ID and driver instance ID.`); + } + const record = requirePayloadRecord(context.kind, canonicalPayload); + const parsed = agentTasksReplacedPayloadSchema.safeParse(omitPayloadIdentity(record)); + if (!parsed.success) { + throw new Error(`Runtime event ${context.kind} payload is invalid.`); + } + return parsed.data; + } case "diagnostic.reported": { const record = requirePayloadRecord(context.kind, canonicalPayload); requireOptionalString(record, "code", context.kind); diff --git a/src/protocol/execution.ts b/src/protocol/execution.ts index 629a91e..dd57655 100644 --- a/src/protocol/execution.ts +++ b/src/protocol/execution.ts @@ -1,6 +1,7 @@ import type { DriverBootMcpServer, DriverExecutionEnvironment, + DriverExecutionSessionContext, DriverExecutionSpec, DriverNativeRuntimeRef, DriverPermissionPolicy, @@ -17,6 +18,7 @@ export interface DriverExecutionRunInput { export interface DriverExecutionSessionInput { readonly additionalDirectories: string[]; + readonly context: DriverExecutionSessionContext; readonly cwd: string; readonly homePath: string; readonly mcpServers: DriverBootMcpServer[]; @@ -55,6 +57,7 @@ export function createDriverExecutionInputFromBootExecution( }, session: { additionalDirectories: execution.session.additionalDirectories, + context: execution.session.context, cwd: execution.session.cwd, homePath: execution.session.context.homePath, mcpServers: execution.session.mcpServers, diff --git a/src/protocol/host-integration.ts b/src/protocol/host-integration.ts deleted file mode 100644 index 8115f34..0000000 --- a/src/protocol/host-integration.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { - DriverConfigRevision, - DriverExecutionSessionContext, - DriverExecutionSpec, -} from "./boot"; - -export interface DriverHostIntegrationSnapshot { - readonly configRevision: DriverConfigRevision; - readonly sessionContext: DriverExecutionSessionContext; -} - -export function createDriverHostIntegrationSnapshotFromBootExecution( - execution: DriverExecutionSpec, -): DriverHostIntegrationSnapshot { - return { - configRevision: execution.configRevision, - sessionContext: execution.session.context, - }; -} diff --git a/src/protocol/id/index.ts b/src/protocol/id/index.ts index 3d36c37..10aa280 100644 --- a/src/protocol/id/index.ts +++ b/src/protocol/id/index.ts @@ -12,8 +12,8 @@ export type SessionId = SemanticDriverId<"SessionId">; export type MessageId = SemanticDriverId<"MessageId">; export type RunId = SemanticDriverId<"RunId">; -export const DRIVER_ID_PATTERN = "^[0-9A-HJKMNP-TV-Z]{26}$"; -export const DRIVER_ID_INPUT_PATTERN = "^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$"; +export const DRIVER_ID_PATTERN = "^[0-7][0-9A-HJKMNP-TV-Z]{25}$"; +export const DRIVER_ID_INPUT_PATTERN = "^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$"; const canonicalDriverIdPattern = new RegExp(DRIVER_ID_PATTERN, "u"); const inputDriverIdPattern = new RegExp(DRIVER_ID_INPUT_PATTERN, "u"); diff --git a/src/protocol/json.ts b/src/protocol/json.ts index cb1d193..9c66829 100644 --- a/src/protocol/json.ts +++ b/src/protocol/json.ts @@ -5,10 +5,19 @@ export interface JsonObject { } export function isJsonObject(value: unknown): value is JsonObject { - return value !== null && typeof value === "object" && !Array.isArray(value); + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; } -function assertJsonValue(value: unknown, label: string): asserts value is JsonValue { +function assertJsonValue( + value: unknown, + label: string, + ancestors = new WeakSet(), +): asserts value is JsonValue { if ( value === null || typeof value === "string" || @@ -19,16 +28,31 @@ function assertJsonValue(value: unknown, label: string): asserts value is JsonVa } if (Array.isArray(value)) { - value.forEach((entry, index) => { - assertJsonValue(entry, `${label}[${index}]`); - }); + if (ancestors.has(value)) { + throw new TypeError(`${label} must be JSON-serializable.`); + } + + ancestors.add(value); + for (let index = 0; index < value.length; index += 1) { + if (!Object.hasOwn(value, index)) { + throw new TypeError(`${label}[${index}] must be JSON-serializable.`); + } + assertJsonValue(value[index], `${label}[${index}]`, ancestors); + } + ancestors.delete(value); return; } if (isJsonObject(value)) { + if (ancestors.has(value)) { + throw new TypeError(`${label} must be JSON-serializable.`); + } + + ancestors.add(value); for (const [key, entry] of Object.entries(value)) { - assertJsonValue(entry, `${label}.${key}`); + assertJsonValue(entry, `${label}.${key}`, ancestors); } + ancestors.delete(value); return; } diff --git a/src/protocol/orpc/index.ts b/src/protocol/orpc/index.ts index 424a347..d790112 100644 --- a/src/protocol/orpc/index.ts +++ b/src/protocol/orpc/index.ts @@ -1,624 +1,466 @@ -import { DRIVER_CAPABILITY_IDS, RUNTIME_COMMAND_STATUSES } from "../../runtime-command"; -import type { - DriverCapability, - DriverCapabilityId, - McpExecuteCommandResult, - McpExternalToolEffectClaim, - RunError, - RuntimeCommand, - RuntimeCommandResult, - RuntimeCommandStatus, -} from "../../runtime-command"; -import { DRIVER_PROTOCOL_VERSION, type DriverBootPayload } from "../boot"; -import { parseDriverEventEnvelope, type DriverEventEnvelope } from "../events"; -import { isSupportedDriverRuntime } from "../runtime"; -import type { DriverRuntime } from "../runtime"; - -export interface DriverHelloInput { - readonly capabilities: readonly DriverCapability[]; - readonly driverVersion: string; - readonly pid: number; - readonly protocolVersion: DriverBootPayload["protocolVersion"]; - readonly runtime: DriverRuntime; - readonly startedAt: string; -} - -export interface DriverHelloOutput { - readonly acceptedCapabilities: readonly DriverCapability[]; - readonly connectionId: string; - readonly driverInstanceId: string; - readonly heartbeatIntervalMs: number; - readonly runConfig: { - readonly commandLeaseMs: number; - readonly envPolicy: "strict"; - readonly eventBatchMaxSize: number; - readonly organizationPath: string; - }; - readonly runId: string | null; -} - -export interface DriverHeartbeatInput { - readonly at: string; - readonly pid: number; - readonly reason: "interval" | "ping"; -} - -export interface DriverHeartbeatOutput { - readonly heartbeatCount: number; - readonly ok: true; -} - -export interface DriverReadyInput { - readonly at: string; - readonly driverInstanceId: string; - readonly pid: number; -} - -export interface DriverLogContext { - parentSpanId?: string | undefined; - requestId?: string | undefined; - sandboxId?: string | undefined; - sessionId?: string | undefined; - spanId?: string | undefined; - traceId?: string | undefined; -} - -export interface DriverLogError { - readonly code?: number | string | undefined; - readonly message: string; - readonly name: string; - readonly stack?: string | null | undefined; -} - -export interface DriverLogEntry { - readonly context?: DriverLogContext | undefined; - readonly error?: DriverLogError | undefined; - readonly fields?: Record | undefined; - readonly level: "debug" | "error" | "info" | "trace" | "warn"; - readonly message: string; - readonly namespace?: string | null | undefined; - readonly seq: number; - readonly timestamp: string; -} - -export interface DriverLogBatchInput { - readonly driverInstanceId: string; - readonly logs: readonly DriverLogEntry[]; -} - -export interface DriverLogBatchOutput { - readonly ok: true; -} - -export interface DriverFailureInput { - readonly driverInstanceId: string; - readonly error: RunError; -} - -export interface DriverCommandUpdateInput { - readonly commandId: string; - readonly driverInstanceId: string; - readonly error?: DriverFailureInput["error"] | undefined; - readonly result?: RuntimeCommandResult | undefined; - readonly status: RuntimeCommandStatus; -} - -export interface DriverExternalToolEffectClaimInput { - readonly commandId: string; - readonly driverInstanceId: string; -} - -export type DriverExternalToolEffectClaimOutput = McpExternalToolEffectClaim; - -export interface DriverExternalToolEffectCompleteInput { - readonly commandId: string; - readonly driverInstanceId: string; - readonly providerReceiptJson?: string | null | undefined; - readonly result: McpExecuteCommandResult; -} - -export interface DriverExternalToolEffectUnknownInput { - readonly commandId: string; - readonly driverInstanceId: string; -} - -export interface DriverEventBatchInput { - readonly driverInstanceId: string; - readonly events: readonly DriverEventEnvelope[]; -} - -export interface DriverEventReceipt { - readonly eventId?: string | undefined; - readonly seq: number; - readonly type: string; -} - -export interface DriverEventBatchOutput { - readonly accepted: readonly DriverEventReceipt[]; -} - -export interface DriverNextCommandInput { - readonly driverInstanceId: string; -} - -export interface DriverNextCommandOutput { - readonly command: RuntimeCommand | null; -} - -export interface DriverCompletionInput { - readonly driverInstanceId: string; -} - -export interface DriverRpcOptions { - readonly signal?: AbortSignal; -} +import { z } from "zod"; -export interface DriverRuntimeClient { - readonly driver: { - commandUpdate( - input: DriverCommandUpdateInput, - options?: DriverRpcOptions, - ): Promise<{ ok: true }>; - claimExternalToolEffect( - input: DriverExternalToolEffectClaimInput, - options?: DriverRpcOptions, - ): Promise; - completeExternalToolEffect( - input: DriverExternalToolEffectCompleteInput, - options?: DriverRpcOptions, - ): Promise<{ ok: true }>; - completeRun(input: DriverCompletionInput, options?: DriverRpcOptions): Promise<{ ok: true }>; - failRun(input: DriverFailureInput, options?: DriverRpcOptions): Promise<{ ok: true }>; - heartbeat( - input: DriverHeartbeatInput, - options?: DriverRpcOptions, - ): Promise; - hello(input: DriverHelloInput, options?: DriverRpcOptions): Promise; - pushEvents( - input: DriverEventBatchInput, - options?: DriverRpcOptions, - ): Promise; - pushLogs(input: DriverLogBatchInput, options?: DriverRpcOptions): Promise; - ready(input: DriverReadyInput, options?: DriverRpcOptions): Promise<{ ok: true }>; - markExternalToolEffectUnknown( - input: DriverExternalToolEffectUnknownInput, - options?: DriverRpcOptions, - ): Promise<{ ok: true }>; - }; - readonly driverInstance: { - nextCommand( - input: DriverNextCommandInput, - options?: DriverRpcOptions, - ): Promise; - }; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function readRecord(value: unknown, label: string): Record { - if (!isRecord(value)) { - throw new TypeError(`${label} must be an object.`); - } - - return value; -} - -function readString(record: Record, field: string): string { - const value = record[field]; - - if (typeof value !== "string") { - throw new TypeError(`${field} must be a string.`); - } - - return value; -} - -function readNonEmptyString(record: Record, field: string): string { - const value = readString(record, field); - - if (value.length === 0) { - throw new TypeError(`${field} must be a non-empty string.`); - } - - return value; -} - -function readOptionalString(record: Record, field: string): string | undefined { - const value = record[field]; - - if (value === undefined) { - return undefined; - } - - if (typeof value !== "string") { - throw new TypeError(`${field} must be a string.`); - } - - return value; -} - -function readNumber(record: Record, field: string): number { - const value = record[field]; - - if (typeof value !== "number" || !Number.isFinite(value)) { - throw new TypeError(`${field} must be a finite number.`); - } - - return value; -} - -function readPositiveInteger(record: Record, field: string): number { - const value = readNumber(record, field); - - if (!Number.isSafeInteger(value) || value < 1) { - throw new TypeError(`${field} must be a positive safe integer.`); - } - - return value; -} - -function readNonNegativeInteger(record: Record, field: string): number { - const value = readNumber(record, field); - - if (!Number.isSafeInteger(value) || value < 0) { - throw new TypeError(`${field} must be a non-negative safe integer.`); - } - - return value; -} - -function readBoolean(record: Record, field: string): boolean { - const value = record[field]; - - if (typeof value !== "boolean") { - throw new TypeError(`${field} must be a boolean.`); - } - - return value; -} - -function readProtocolVersion( - record: Record, -): DriverBootPayload["protocolVersion"] { - const value = record["protocolVersion"]; - - if (value !== DRIVER_PROTOCOL_VERSION) { - throw new TypeError(`protocolVersion must be ${DRIVER_PROTOCOL_VERSION}.`); - } - - return value; -} - -function readDriverRuntime(record: Record): DriverRuntime { - const runtime = readNonEmptyString(record, "runtime"); - - if (!isSupportedDriverRuntime(runtime)) { - throw new TypeError(`Unsupported driver runtime: ${runtime}.`); +import { + DRIVER_CAPABILITY_IDS, + parseRuntimeCommand, + RUNTIME_COMMAND_STATUSES, +} from "../../runtime-command"; +import { DRIVER_PROTOCOL_VERSION } from "../boot"; +import { parseDriverEventEnvelope } from "../events"; +import { SUPPORTED_DRIVER_RUNTIMES } from "../runtime"; + +const nonEmptyStringSchema = z.string().min(1); +const primitiveSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +const okSchema = z.object({ ok: z.literal(true) }); + +function omitUndefined>(value: T): T { + return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T; +} + +function positiveSafeIntegerSchema(label: string, minimum = 1) { + return z.number().refine((value) => Number.isSafeInteger(value) && value >= minimum, { + error: + minimum === 1 + ? `${label} must be a positive safe integer.` + : `${label} must be an integer of at least ${String(minimum)}.`, + }); +} + +function nonNegativeSafeIntegerSchema(label: string) { + return z.number().refine((value) => Number.isSafeInteger(value) && value >= 0, { + error: `${label} must be a non-negative safe integer.`, + }); +} + +function parsedSchema(parser: (value: unknown) => Output) { + return z.unknown().transform((value, ctx) => { + try { + return parser(value); + } catch (error) { + if (error instanceof z.ZodError) { + for (const issue of error.issues) { + ctx.addIssue({ code: "custom", message: issue.message, path: issue.path }); + } + } else { + ctx.addIssue({ + code: "custom", + message: error instanceof Error ? error.message : "Invalid value.", + }); + } + + return z.NEVER; + } + }); +} + +function primitiveRecordSchema(label: string) { + return z + .custom>( + (value) => typeof value === "object" && value !== null && !Array.isArray(value), + { error: `${label} must be an object.` }, + ) + .superRefine((record, ctx) => { + for (const [field, value] of Object.entries(record)) { + if (!primitiveSchema.safeParse(value).success) { + ctx.addIssue({ + code: "custom", + message: `${label}.${field} must be a primitive value.`, + path: [field], + }); + } + } + }) + .transform( + (record) => + Object.fromEntries(Object.entries(record)) as Record< + string, + string | number | boolean | null + >, + ); +} + +const driverCapabilitySchema = z + .object({ + details: z.string().optional(), + id: z.enum(DRIVER_CAPABILITY_IDS, { error: "capability id is unsupported." }), + status: z.enum(["supported", "unsupported"], { + error: "capability status must be supported or unsupported.", + }), + version: z.literal(1, { error: "capability version must be 1." }), + }) + .transform(omitUndefined); + +const driverCapabilitiesSchema = z + .array(driverCapabilitySchema) + .superRefine((capabilities, ctx) => { + if (new Set(capabilities.map(({ id }) => id)).size !== capabilities.length) { + ctx.addIssue({ + code: "custom", + message: "capabilities must not contain duplicate ids.", + }); + } + }); + +const runErrorSchema = z.object({ + code: nonEmptyStringSchema, + details: primitiveRecordSchema("driver failure details"), + message: z.string(), + retryable: z.boolean(), +}); + +const inputStartCommandResultSchema = z.object({ + requestId: nonEmptyStringSchema, +}); + +const mcpExecuteCommandResultSchema = z + .object({ + isError: z.boolean().optional(), + outputText: z.string(), + requestId: nonEmptyStringSchema, + serverId: nonEmptyStringSchema, + toolName: nonEmptyStringSchema, + }) + .transform(omitUndefined); + +const runtimeCommandResultSchema = z.unknown().transform((value, ctx) => { + if (value === null) { + return null; } - return runtime; -} - -function readHeartbeatReason(value: unknown): DriverHeartbeatInput["reason"] { - if (value === "interval" || value === "ping") { - return value; + const record = value as Record; + const result = ( + record !== null && + typeof record === "object" && + !Array.isArray(record) && + ["isError", "outputText", "serverId", "toolName"].some((field) => Object.hasOwn(record, field)) + ? mcpExecuteCommandResultSchema + : inputStartCommandResultSchema + ).safeParse(value); + + if (result.success) { + return result.data; } - throw new TypeError("reason must be interval or ping."); -} - -function readDriverCapabilityId(value: unknown): DriverCapabilityId { - if (typeof value === "string" && DRIVER_CAPABILITY_IDS.includes(value as DriverCapabilityId)) { - return value as DriverCapabilityId; + for (const issue of result.error.issues) { + ctx.addIssue({ code: "custom", message: issue.message, path: issue.path }); } + return z.NEVER; +}); + +const driverHelloInputSchema = z.object({ + capabilities: driverCapabilitiesSchema, + driverVersion: nonEmptyStringSchema, + pid: positiveSafeIntegerSchema("pid"), + protocolVersion: z.literal(DRIVER_PROTOCOL_VERSION, { + error: `protocolVersion must be ${String(DRIVER_PROTOCOL_VERSION)}.`, + }), + runtime: z.enum(SUPPORTED_DRIVER_RUNTIMES, { error: "Unsupported driver runtime." }), + startedAt: nonEmptyStringSchema, +}); + +const driverHelloOutputSchema = z.object({ + acceptedCapabilities: driverCapabilitiesSchema, + connectionId: nonEmptyStringSchema, + driverInstanceId: nonEmptyStringSchema, + heartbeatIntervalMs: positiveSafeIntegerSchema("Driver heartbeat interval", 250), + runConfig: z.object({ + commandLeaseMs: z.number().nonnegative(), + envPolicy: z.literal("strict"), + eventBatchMaxSize: positiveSafeIntegerSchema("Driver event batch max size"), + organizationPath: nonEmptyStringSchema, + }), + runId: nonEmptyStringSchema.nullable(), +}); + +const driverHeartbeatInputSchema = z.object({ + at: nonEmptyStringSchema, + pid: positiveSafeIntegerSchema("pid"), + reason: z.enum(["interval", "ping"], { error: "reason must be interval or ping." }), +}); + +const driverHeartbeatOutputSchema = z.object({ + heartbeatCount: nonNegativeSafeIntegerSchema("heartbeatCount"), + ok: z.literal(true), +}); + +const driverReadyInputSchema = z.object({ + at: nonEmptyStringSchema, + driverInstanceId: nonEmptyStringSchema, + pid: positiveSafeIntegerSchema("pid"), +}); + +const driverLogContextSchema = z + .object({ + parentSpanId: z.string().optional(), + requestId: z.string().optional(), + sandboxId: z.string().optional(), + sessionId: z.string().optional(), + spanId: z.string().optional(), + traceId: z.string().optional(), + }) + .transform(omitUndefined); + +const driverLogErrorSchema = z + .object({ + code: z.union([z.string(), z.number()]).optional(), + message: z.string(), + name: z.string(), + stack: z.string().nullable().optional(), + }) + .transform(omitUndefined); + +const driverLogEntrySchema = z + .object({ + context: driverLogContextSchema.optional(), + error: driverLogErrorSchema.optional(), + fields: primitiveRecordSchema("driver log fields").optional(), + level: z.enum(["debug", "error", "info", "trace", "warn"], { + error: "driver log level is unsupported.", + }), + message: z.string(), + namespace: z.string().nullable().optional(), + seq: nonNegativeSafeIntegerSchema("seq"), + timestamp: nonEmptyStringSchema, + }) + .transform(omitUndefined); + +const driverLogBatchInputSchema = z.object({ + driverInstanceId: nonEmptyStringSchema, + logs: z.array(driverLogEntrySchema), +}); + +const driverFailureInputSchema = z.object({ + driverInstanceId: nonEmptyStringSchema, + error: runErrorSchema, +}); + +const driverCommandUpdateInputSchema = z + .object({ + commandId: nonEmptyStringSchema, + driverInstanceId: nonEmptyStringSchema, + error: runErrorSchema.optional(), + result: runtimeCommandResultSchema.optional(), + status: z.enum(RUNTIME_COMMAND_STATUSES, { + error: "status is not a supported runtime command status.", + }), + }) + .transform(omitUndefined); + +const driverExternalToolEffectClaimInputSchema = z.object({ + commandId: nonEmptyStringSchema, + driverInstanceId: nonEmptyStringSchema, +}); + +const driverExternalToolEffectClaimOutputSchema = z.discriminatedUnion("kind", [ + z.object({ + attempt: positiveSafeIntegerSchema("attempt"), + effectId: nonEmptyStringSchema, + idempotencyKey: nonEmptyStringSchema, + kind: z.literal("execute"), + }), + z.object({ + effectId: nonEmptyStringSchema, + kind: z.literal("completed"), + result: mcpExecuteCommandResultSchema, + }), + z.object({ + effectId: nonEmptyStringSchema, + kind: z.literal("unknown"), + }), +]); + +const driverExternalToolEffectCompleteInputSchema = z + .object({ + commandId: nonEmptyStringSchema, + driverInstanceId: nonEmptyStringSchema, + providerReceiptJson: z.string().nullable().optional(), + result: mcpExecuteCommandResultSchema, + }) + .transform(omitUndefined); + +const driverExternalToolEffectUnknownInputSchema = z.object({ + commandId: nonEmptyStringSchema, + driverInstanceId: nonEmptyStringSchema, +}); + +const driverEventBatchInputSchema = z.object({ + driverInstanceId: nonEmptyStringSchema, + events: z.array(parsedSchema(parseDriverEventEnvelope)), +}); + +const driverEventReceiptSchema = z + .object({ + eventId: z.string().optional(), + seq: nonNegativeSafeIntegerSchema("Driver event receipt seq"), + type: z.string(), + }) + .transform(omitUndefined); + +const driverEventBatchOutputSchema = z.object({ + accepted: z.array(driverEventReceiptSchema), +}); + +const driverInstanceInputSchema = z.object({ + driverInstanceId: nonEmptyStringSchema, +}); + +const driverNextCommandOutputSchema = z.object({ + command: parsedSchema(parseRuntimeCommand).nullable(), +}); + +interface RpcMethodSchema { + readonly input: z.ZodType; + readonly output: z.ZodType; +} + +type RpcSchemaMap = Record>; + +type DeepReadonly = Value extends + | string + | number + | boolean + | bigint + | symbol + | null + | undefined + ? Value + : Value extends readonly (infer Entry)[] + ? readonly DeepReadonly[] + : Value extends object + ? { readonly [Key in keyof Value]: DeepReadonly } + : Value; + +type SchemaValue = DeepReadonly>; + +export const driverRuntimeRpcSchemas = { + driver: { + claimExternalToolEffect: { + input: driverExternalToolEffectClaimInputSchema, + output: driverExternalToolEffectClaimOutputSchema, + }, + commandUpdate: { input: driverCommandUpdateInputSchema, output: okSchema }, + completeExternalToolEffect: { + input: driverExternalToolEffectCompleteInputSchema, + output: okSchema, + }, + completeRun: { input: driverInstanceInputSchema, output: okSchema }, + failRun: { input: driverFailureInputSchema, output: okSchema }, + heartbeat: { input: driverHeartbeatInputSchema, output: driverHeartbeatOutputSchema }, + hello: { input: driverHelloInputSchema, output: driverHelloOutputSchema }, + markExternalToolEffectUnknown: { + input: driverExternalToolEffectUnknownInputSchema, + output: okSchema, + }, + pushEvents: { input: driverEventBatchInputSchema, output: driverEventBatchOutputSchema }, + pushLogs: { input: driverLogBatchInputSchema, output: okSchema }, + ready: { input: driverReadyInputSchema, output: okSchema }, + }, + driverInstance: { + nextCommand: { input: driverInstanceInputSchema, output: driverNextCommandOutputSchema }, + }, +} as const satisfies RpcSchemaMap; - throw new TypeError("capability id is unsupported."); -} - -function readDriverCapabilityStatus(value: unknown): DriverCapability["status"] { - if (value === "supported" || value === "unsupported") { - return value; - } - - throw new TypeError("capability status must be supported or unsupported."); +export interface DriverRpcOptions { + readonly signal?: AbortSignal; } -function readDriverCapability(value: unknown): DriverCapability { - const record = readRecord(value, "capability"); - const details = readOptionalString(record, "details"); - - return { - ...(details === undefined ? {} : { details }), - id: readDriverCapabilityId(record["id"]), - status: readDriverCapabilityStatus(record["status"]), - version: readDriverCapabilityVersion(record["version"]), +type RpcClient = { + readonly [Group in keyof Schema]: { + readonly [Method in keyof Schema[Group]]: Schema[Group][Method] extends RpcMethodSchema + ? ( + input: SchemaValue, + options?: DriverRpcOptions, + ) => Promise> + : never; }; -} - -function readDriverCapabilityVersion(value: unknown): 1 { - if (value !== 1) { - throw new TypeError("capability version must be 1."); - } - - return value; -} - -function readDriverCapabilities(record: Record): DriverCapability[] { - const value = record["capabilities"]; - - if (!Array.isArray(value)) { - throw new TypeError("capabilities must be an array."); - } - - const capabilities = value.map(readDriverCapability); - - if (new Set(capabilities.map(({ id }) => id)).size !== capabilities.length) { - throw new TypeError("capabilities must not contain duplicate ids."); - } - - return capabilities; -} +}; + +export type DriverRuntimeClient = RpcClient; +export type DriverHelloInput = SchemaValue; +export type DriverHelloOutput = SchemaValue; +export type DriverHeartbeatInput = SchemaValue; +export type DriverHeartbeatOutput = SchemaValue; +export type DriverReadyInput = SchemaValue; +export type DriverLogContext = z.output; +export type DriverLogError = SchemaValue; +export type DriverLogEntry = Omit, "context"> & { + readonly context?: DriverLogContext | undefined; +}; +export type DriverLogBatchInput = Omit, "logs"> & { + readonly logs: readonly DriverLogEntry[]; +}; +export type DriverLogBatchOutput = SchemaValue; +export type DriverFailureInput = SchemaValue; +export type DriverCommandUpdateInput = SchemaValue; +export type DriverExternalToolEffectClaimInput = SchemaValue< + typeof driverExternalToolEffectClaimInputSchema +>; +export type DriverExternalToolEffectClaimOutput = SchemaValue< + typeof driverExternalToolEffectClaimOutputSchema +>; +export type DriverExternalToolEffectCompleteInput = SchemaValue< + typeof driverExternalToolEffectCompleteInputSchema +>; +export type DriverExternalToolEffectUnknownInput = SchemaValue< + typeof driverExternalToolEffectUnknownInputSchema +>; +export type DriverEventBatchInput = SchemaValue; +export type DriverEventReceipt = SchemaValue; +export type DriverEventBatchOutput = SchemaValue; +export type DriverNextCommandInput = SchemaValue; +export type DriverNextCommandOutput = SchemaValue; +export type DriverCompletionInput = SchemaValue; export function parseDriverHelloInput(value: unknown): DriverHelloInput { - const record = readRecord(value, "driver hello input"); - - return { - capabilities: readDriverCapabilities(record), - driverVersion: readNonEmptyString(record, "driverVersion"), - pid: readPositiveInteger(record, "pid"), - protocolVersion: readProtocolVersion(record), - runtime: readDriverRuntime(record), - startedAt: readNonEmptyString(record, "startedAt"), - }; + return driverHelloInputSchema.parse(value); } export function parseDriverHeartbeatInput(value: unknown): DriverHeartbeatInput { - const record = readRecord(value, "driver heartbeat input"); - - return { - at: readNonEmptyString(record, "at"), - pid: readPositiveInteger(record, "pid"), - reason: readHeartbeatReason(record["reason"]), - }; + return driverHeartbeatInputSchema.parse(value); } export function parseDriverReadyInput(value: unknown): DriverReadyInput { - const record = readRecord(value, "driver ready input"); - - return { - at: readNonEmptyString(record, "at"), - driverInstanceId: readNonEmptyString(record, "driverInstanceId"), - pid: readPositiveInteger(record, "pid"), - }; + return driverReadyInputSchema.parse(value); } -function readDriverCommandStatus(value: unknown): RuntimeCommandStatus { - if ( - typeof value === "string" && - RUNTIME_COMMAND_STATUSES.includes(value as RuntimeCommandStatus) - ) { - return value as RuntimeCommandStatus; - } - - throw new TypeError("status is not a supported runtime command status."); +export function parseDriverCommandUpdateInput(value: unknown): DriverCommandUpdateInput { + return driverCommandUpdateInputSchema.parse(value); } -function readPrimitiveRecord( +export function parseDriverExternalToolEffectClaimInput( value: unknown, - label: string, -): Record { - const record = readRecord(value, label); - - for (const [field, entry] of Object.entries(record)) { - if ( - entry !== null && - typeof entry !== "string" && - typeof entry !== "boolean" && - (typeof entry !== "number" || !Number.isFinite(entry)) - ) { - throw new TypeError(`${label}.${field} must be a primitive value.`); - } - } - - return { ...record } as Record; -} - -function readDriverFailure(value: unknown): DriverFailureInput["error"] { - const record = readRecord(value, "driver failure"); - - return { - code: readNonEmptyString(record, "code"), - details: readPrimitiveRecord(record["details"], "driver failure details"), - message: readString(record, "message"), - retryable: readBoolean(record, "retryable"), - }; +): DriverExternalToolEffectClaimInput { + return driverExternalToolEffectClaimInputSchema.parse(value); } -function readRuntimeCommandResult(value: unknown): RuntimeCommandResult { - if (value === null) { - return null; - } - - const record = readRecord(value, "runtime command result"); - const requestId = readNonEmptyString(record, "requestId"); - - if ( - record["outputText"] !== undefined || - record["serverId"] !== undefined || - record["toolName"] !== undefined - ) { - const isError = record["isError"] === undefined ? undefined : readBoolean(record, "isError"); - - return { - ...(isError === undefined ? {} : { isError }), - outputText: readString(record, "outputText"), - requestId, - serverId: readNonEmptyString(record, "serverId"), - toolName: readNonEmptyString(record, "toolName"), - }; - } - - return { requestId }; +export function parseDriverExternalToolEffectCompleteInput( + value: unknown, +): DriverExternalToolEffectCompleteInput { + return driverExternalToolEffectCompleteInputSchema.parse(value); } -export function parseDriverCommandUpdateInput(value: unknown): DriverCommandUpdateInput { - const record = readRecord(value, "driver command update input"); - const error = record["error"] === undefined ? undefined : readDriverFailure(record["error"]); - const result = - record["result"] === undefined ? undefined : readRuntimeCommandResult(record["result"]); - - return { - commandId: readNonEmptyString(record, "commandId"), - driverInstanceId: readNonEmptyString(record, "driverInstanceId"), - ...(error === undefined ? {} : { error }), - ...(result === undefined ? {} : { result }), - status: readDriverCommandStatus(record["status"]), - }; +export function parseDriverExternalToolEffectUnknownInput( + value: unknown, +): DriverExternalToolEffectUnknownInput { + return driverExternalToolEffectUnknownInputSchema.parse(value); } export function parseDriverCompletionInput(value: unknown): DriverCompletionInput { - const record = readRecord(value, "driver completion input"); - return { driverInstanceId: readNonEmptyString(record, "driverInstanceId") }; + return driverInstanceInputSchema.parse(value); } export function parseDriverFailureInput(value: unknown): DriverFailureInput { - const record = readRecord(value, "driver failure input"); - - return { - driverInstanceId: readNonEmptyString(record, "driverInstanceId"), - error: readDriverFailure(record["error"]), - }; -} - -function readOptionalLogContext(value: unknown): DriverLogContext | undefined { - if (value === undefined) { - return undefined; - } - - const record = readRecord(value, "driver log context"); - const context = { - parentSpanId: readOptionalString(record, "parentSpanId"), - requestId: readOptionalString(record, "requestId"), - sandboxId: readOptionalString(record, "sandboxId"), - sessionId: readOptionalString(record, "sessionId"), - spanId: readOptionalString(record, "spanId"), - traceId: readOptionalString(record, "traceId"), - }; - - return Object.fromEntries( - Object.entries(context).filter((entry): entry is [string, string] => entry[1] !== undefined), - ); -} - -function readOptionalLogError(value: unknown): DriverLogError | undefined { - if (value === undefined) { - return undefined; - } - - const record = readRecord(value, "driver log error"); - const code = record["code"]; - const stack = record["stack"]; - - if ( - code !== undefined && - typeof code !== "string" && - (typeof code !== "number" || !Number.isFinite(code)) - ) { - throw new TypeError("driver log error.code must be a string or finite number."); - } - - if (stack !== undefined && stack !== null && typeof stack !== "string") { - throw new TypeError("driver log error.stack must be a string or null."); - } - - return { - ...(code === undefined ? {} : { code }), - message: readString(record, "message"), - name: readString(record, "name"), - ...(stack === undefined ? {} : { stack }), - }; -} - -function readOptionalNullableString( - record: Record, - field: string, -): string | null | undefined { - const value = record[field]; - - if (value === undefined || value === null || typeof value === "string") { - return value; - } - - throw new TypeError(`${field} must be a string or null.`); -} - -function readDriverLogLevel(value: unknown): DriverLogEntry["level"] { - if ( - value === "debug" || - value === "error" || - value === "info" || - value === "trace" || - value === "warn" - ) { - return value; - } - - throw new TypeError("driver log level is unsupported."); -} - -function readDriverLogEntry(value: unknown): DriverLogEntry { - const record = readRecord(value, "driver log entry"); - const context = readOptionalLogContext(record["context"]); - const error = readOptionalLogError(record["error"]); - const fields = - record["fields"] === undefined - ? undefined - : readPrimitiveRecord(record["fields"], "driver log fields"); - const namespace = readOptionalNullableString(record, "namespace"); - - return { - ...(context === undefined ? {} : { context }), - ...(error === undefined ? {} : { error }), - ...(fields === undefined ? {} : { fields }), - level: readDriverLogLevel(record["level"]), - message: readString(record, "message"), - ...(namespace === undefined ? {} : { namespace }), - seq: readNonNegativeInteger(record, "seq"), - timestamp: readNonEmptyString(record, "timestamp"), - }; + return driverFailureInputSchema.parse(value); } export function parseDriverLogBatchInput(value: unknown): DriverLogBatchInput { - const record = readRecord(value, "driver log batch input"); - const logs = record["logs"]; - - if (!Array.isArray(logs)) { - throw new TypeError("logs must be an array."); - } - - return { - driverInstanceId: readNonEmptyString(record, "driverInstanceId"), - logs: logs.map(readDriverLogEntry), - }; + return driverLogBatchInputSchema.parse(value); } export function parseDriverEventBatchInput(value: unknown): DriverEventBatchInput { - const record = readRecord(value, "driver event batch input"); - const events = record["events"]; - - if (!Array.isArray(events)) { - throw new TypeError("events must be an array."); - } - - return { - driverInstanceId: readNonEmptyString(record, "driverInstanceId"), - events: events.map(parseDriverEventEnvelope), - }; + return driverEventBatchInputSchema.parse(value); } export function parseDriverNextCommandInput(value: unknown): DriverNextCommandInput { - const record = readRecord(value, "driver next command input"); - return { driverInstanceId: readNonEmptyString(record, "driverInstanceId") }; + return driverInstanceInputSchema.parse(value); } diff --git a/src/protocol/runtime.ts b/src/protocol/runtime.ts index 23400dc..0160f27 100644 --- a/src/protocol/runtime.ts +++ b/src/protocol/runtime.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export const SUPPORTED_DRIVER_RUNTIMES = [ "openai-runtime", "claude-agent-sdk", @@ -20,12 +22,6 @@ export type DriverRuntime = (typeof SUPPORTED_DRIVER_RUNTIMES)[number]; export type DriverRuntimeTransport = (typeof SUPPORTED_DRIVER_RUNTIME_TRANSPORTS)[number]; export type DriverNativeRuntimeRefKind = (typeof SUPPORTED_DRIVER_NATIVE_RUNTIME_REF_KINDS)[number]; -export interface DriverNativeRuntimeRef { - readonly kind: DriverNativeRuntimeRefKind; - readonly runtimeId: DriverRuntime; - readonly value: string; -} - export function isSupportedDriverRuntime(value: string): value is DriverRuntime { return (SUPPORTED_DRIVER_RUNTIMES as readonly string[]).includes(value); } @@ -50,42 +46,53 @@ export function getExpectedDriverNativeRuntimeRefKind( } } -function readRecord(value: unknown, label: string): Record { - if (typeof value !== "object" || value === null || Array.isArray(value)) { - throw new TypeError(`${label} must be an object.`); - } - - return value as Record; -} - -function readNonEmptyString(record: Record, field: string, label: string): string { - const value = record[field]; - - if (typeof value !== "string" || value.length === 0) { - throw new TypeError(`${label}.${field} must be a non-empty string.`); - } +const nativeRuntimeRefKeys = ["kind", "runtimeId", "value"] as const; +const driverNativeRuntimeRefSchema = z.preprocess( + (value) => { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return value; + } - return value; -} + const record = value as Record; + return Object.fromEntries( + nativeRuntimeRefKeys + .filter((key) => Object.hasOwn(record, key)) + .map((key) => [key, record[key]]), + ); + }, + z + .object({ + kind: z.enum(SUPPORTED_DRIVER_NATIVE_RUNTIME_REF_KINDS), + runtimeId: z.enum(SUPPORTED_DRIVER_RUNTIMES), + value: z.string().min(1), + }) + .superRefine((reference, context) => { + if (reference.kind !== getExpectedDriverNativeRuntimeRefKind(reference.runtimeId)) { + context.addIssue({ + code: "custom", + message: `kind ${reference.kind} does not match runtime ${reference.runtimeId}`, + path: ["kind"], + }); + } + }), +); + +export type DriverNativeRuntimeRef = Readonly>; export function parseDriverNativeRuntimeRef(value: unknown): DriverNativeRuntimeRef { - const record = readRecord(value, "Driver native runtime ref"); - const runtimeId = readNonEmptyString(record, "runtimeId", "Driver native runtime ref"); - const kind = readNonEmptyString(record, "kind", "Driver native runtime ref"); + try { + const result = driverNativeRuntimeRefSchema.safeParse(value); - if (!isSupportedDriverRuntime(runtimeId)) { - throw new TypeError(`Unsupported native runtime ref runtime: ${runtimeId}.`); - } + if (!result.success) { + throw new TypeError(z.prettifyError(result.error)); + } - const expectedKind = getExpectedDriverNativeRuntimeRefKind(runtimeId); + return result.data; + } catch (error) { + if (error instanceof TypeError) { + throw error; + } - if (kind !== expectedKind) { - throw new TypeError(`Native runtime ref kind ${kind} does not match runtime ${runtimeId}.`); + throw new TypeError("Driver native runtime ref could not be read.", { cause: error }); } - - return { - kind, - runtimeId, - value: readNonEmptyString(record, "value", "Driver native runtime ref"), - }; } diff --git a/src/runtimes/acp/acp-driver-backend.ts b/src/runtimes/acp/acp-driver-backend.ts index b41ef05..2f14d3d 100644 --- a/src/runtimes/acp/acp-driver-backend.ts +++ b/src/runtimes/acp/acp-driver-backend.ts @@ -14,7 +14,6 @@ import { Readable, Writable } from "node:stream"; import { summarizePath, summarizePathCollection } from "../../observability/driver-debug"; import type { DriverEventInput } from "../../protocol/events"; -import type { DriverHostIntegrationSnapshot } from "../../protocol/host-integration"; import type { RunId } from "../../protocol/id"; import type { DriverRuntime } from "../../protocol/runtime"; import type { DriverStartInput } from "../../protocol/start"; @@ -76,7 +75,6 @@ export class AcpDriverBackend implements AgentDriverBackend { readonly #clientRequests: AcpClientRequestHandler; #connection: ClientConnection | null = null; readonly #eventPublisher = new DriverEventPublisher(this.runtime, () => this.#nativeSessionId); - #hostSnapshot: DriverHostIntegrationSnapshot | null = null; #nativeSessionId: string | null = null; #nativeInstructionPath: string | null = null; readonly #payload: DriverStartInput; @@ -122,13 +120,6 @@ export class AcpDriverBackend implements AgentDriverBackend { try { await raceWithAbort(this.#clientRequests.initializePathScope(), signal); - const hostSnapshot = await raceWithAbort(context.ports.hostIntegration.snapshot(), signal); - - if (hostSnapshot === null) { - throw new Error("ACP fallback requires a host integration snapshot."); - } - - this.#hostSnapshot = hostSnapshot; const materializedSkills = await context.ports.skill.materialize( this.#payload.execution, signal, @@ -432,7 +423,6 @@ export class AcpDriverBackend implements AgentDriverBackend { runId, this.#requireConnection(), this.#requireSessionId(), - this.#requireHostSnapshot(), this.#clientRequests, signal, ); @@ -723,14 +713,6 @@ export class AcpDriverBackend implements AgentDriverBackend { return this.#nativeSessionId; } - #requireHostSnapshot(): DriverHostIntegrationSnapshot { - if (this.#hostSnapshot === null) { - throw new Error("ACP driver backend host integration snapshot is not initialized."); - } - - return this.#hostSnapshot; - } - async #joinStop(): Promise { if (this.#stopFailure !== null) { throw this.#stopFailure.error; @@ -803,7 +785,6 @@ export class AcpDriverBackend implements AgentDriverBackend { } async #setupSession(signal: AbortSignal): Promise>> { - const hostSnapshot = this.#requireHostSnapshot(); signal.throwIfAborted(); const deferredUpdates = this.#nativeSessionId === null || @@ -819,7 +800,6 @@ export class AcpDriverBackend implements AgentDriverBackend { connection: this.#requireConnection(), currentSessionId: this.#nativeSessionId, payload: this.#payload, - sessionContext: hostSnapshot.sessionContext, replaySession: async (operation) => this.#clientRequests.withSessionReplay(operation), }), signal, diff --git a/src/runtimes/acp/acp-session-setup.ts b/src/runtimes/acp/acp-session-setup.ts index 445d602..d804e78 100644 --- a/src/runtimes/acp/acp-session-setup.ts +++ b/src/runtimes/acp/acp-session-setup.ts @@ -7,7 +7,6 @@ import type { ResumeSessionRequest, } from "@agentclientprotocol/sdk"; -import type { DriverExecutionSessionContext } from "../../protocol/boot"; import type { DriverStartInput } from "../../protocol/start"; import { buildMcpServers, @@ -34,7 +33,6 @@ interface AcpSessionSetupInput { readonly connection: ClientContext; readonly currentSessionId: string | null; readonly payload: DriverStartInput; - readonly sessionContext: DriverExecutionSessionContext; replaySession(operation: () => Promise): Promise; } @@ -87,7 +85,7 @@ export async function setupAcpSession(input: AcpSessionSetupInput): Promise { @@ -285,7 +283,7 @@ export class AcpTurnController { active.providerPromptAdmitted = true; const promptResult = await connection.request(acpMethods.agent.session.prompt, { _meta: { - ...toRequestMeta({ sessionContext: hostSnapshot.sessionContext }), + ...toRequestMeta({ sessionContext: context.payload.execution.session.context }), "mosoo.ai/messageId": messageId, }, prompt: [{ text: input.text, type: "text" }], diff --git a/src/runtimes/claude/agent-sdk-message-translator.ts b/src/runtimes/claude/agent-sdk-message-translator.ts index 8aa90b1..882d0b5 100644 --- a/src/runtimes/claude/agent-sdk-message-translator.ts +++ b/src/runtimes/claude/agent-sdk-message-translator.ts @@ -28,7 +28,10 @@ import { isClaudeResultSuccessful, } from "./agent-sdk-outcomes"; import { ClaudeAgentSdkMessageState, readClaudeSdkSessionId } from "./agent-sdk-message-state"; -import { ClaudeAgentSdkTaskEvents } from "./agent-sdk-task-events"; +import { + claudeBackgroundTasksClosedEvent, + projectClaudeBackgroundTasksSnapshot, +} from "./agent-sdk-task-events"; import { isToolUseBlock, toToolCallId, toToolCallName, toToolResultText } from "./agent-sdk-tools"; import type { ClaudePermissionDenialAdvisory } from "./agent-sdk-outcomes"; interface ClaudeMessageTranslatorOptions { @@ -67,18 +70,15 @@ export class ClaudeAgentSdkMessageTranslator { readonly #options: ClaudeMessageTranslatorOptions; readonly #permissionDenialAdvisories = new Map(); readonly #state = new ClaudeAgentSdkMessageState(); - readonly #tasks: ClaudeAgentSdkTaskEvents; constructor(options: ClaudeMessageTranslatorOptions) { this.#options = options; this.#events = new ClaudeAgentSdkEventWriter({ push: options.push }); - this.#tasks = new ClaudeAgentSdkTaskEvents(); } resetTurnMessageState(): void { this.#state.reset(); this.#events.resetTurnState(); - this.#tasks.reset(); this.#permissionDenialAdvisories.clear(); } @@ -94,13 +94,9 @@ export class ClaudeAgentSdkMessageTranslator { } await this.#events.finishTools(context, "cancelled"); - const taskClosure = this.#tasks.prepareClosure(); - for (const [index, event] of taskClosure.events.entries()) { - taskClosure.beforePublish(index); - await this.#options.push(context, "driver.claude.tasks.finished", [event]); - taskClosure.commitAccepted(index); - } - taskClosure.commit(); + await this.#options.push(context, "driver.claude.tasks.finished", [ + claudeBackgroundTasksClosedEvent(), + ]); } async finishTurnWithTerminal( @@ -110,15 +106,13 @@ export class ClaudeAgentSdkMessageTranslator { reason: string, ): Promise { const closure = this.#events.prepareTurnClosure(toolStatus); - const taskClosure = this.#tasks.prepareClosure(); await this.#options.pushTerminal( context, reason, - [...closure.events, ...taskClosure.events], + [...closure.events, claudeBackgroundTasksClosedEvent()], terminal, ); closure.commit(); - taskClosure.commit(); this.#state.clearThoughtIds(); for (const [messageId, runId] of this.#state.assistantMessages()) { @@ -693,13 +687,11 @@ export class ClaudeAgentSdkMessageTranslator { context: AgentDriverContext, message: Extract, ): Promise { - const projection = this.#tasks.prepare(message); - for (const [index, event] of projection.events.entries()) { - projection.beforePublish(index); - await this.#options.push(context, "driver.claude.task.updated", [event]); - projection.commitAccepted(index); - } - projection.commit(); + const { diagnostic, snapshot } = projectClaudeBackgroundTasksSnapshot(message); + await this.#options.push(context, "driver.claude.tasks.replaced", [ + ...(snapshot === null ? [] : [snapshot]), + ...(diagnostic === undefined ? [] : [diagnostic]), + ]); } async #retractWireItems( diff --git a/src/runtimes/claude/agent-sdk-task-events.ts b/src/runtimes/claude/agent-sdk-task-events.ts index 2b31ce3..5bf9ac9 100644 --- a/src/runtimes/claude/agent-sdk-task-events.ts +++ b/src/runtimes/claude/agent-sdk-task-events.ts @@ -3,32 +3,22 @@ import { createHash } from "node:crypto"; import type { SDKBackgroundTasksChangedMessage } from "@anthropic-ai/claude-agent-sdk"; import type { DriverEventInput } from "../../protocol/events"; -import { assertClaudeDurableEventFits } from "./agent-sdk-event-writer"; - -interface ClaudeBackgroundTask { - readonly taskType?: string | undefined; - readonly title?: string | undefined; -} - -export interface ClaudeTaskProjection { - readonly beforePublish: (index: number) => void; - readonly commit: () => void; - readonly commitAccepted: (index: number) => void; - readonly events: readonly DriverEventInput[]; -} +import { + assertClaudeDurableEventFits, + ClaudeDurableEventTooLargeError, +} from "./agent-sdk-event-writer"; const MAX_CLAUDE_TASK_ID_BYTES = 256; const MAX_CLAUDE_BACKGROUND_TASK_ENTRIES = 1_024; const MAX_CLAUDE_TASK_TEXT_LENGTH = 4_096; const MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS = 256; -const EMPTY_CLAUDE_TASK_PROJECTION: ClaudeTaskProjection = { - beforePublish: () => {}, - commit: () => {}, - commitAccepted: () => {}, - events: [], -}; - -function rejectedTaskSnapshot(code: string, taskCount: number): ClaudeTaskProjection { + +export interface ClaudeBackgroundTasksProjection { + readonly diagnostic?: DriverEventInput; + readonly snapshot: DriverEventInput | null; +} + +function taskSnapshotDiagnostic(code: string, taskCount: number): DriverEventInput { const event: DriverEventInput = { delivery: "best_effort", kind: "diagnostic.reported", @@ -42,7 +32,11 @@ function rejectedTaskSnapshot(code: string, taskCount: number): ClaudeTaskProjec visibility: "owner_debug", }; assertClaudeDurableEventFits(event, code, "background task snapshot diagnostic"); - return { ...EMPTY_CLAUDE_TASK_PROJECTION, events: [event] }; + return event; +} + +function rejectedTaskSnapshot(code: string, taskCount: number): ClaudeBackgroundTasksProjection { + return { diagnostic: taskSnapshotDiagnostic(code, taskCount), snapshot: null }; } function publicTaskId(nativeTaskId: string): string | null { @@ -62,145 +56,77 @@ function boundedTaskText(value: string): string | undefined { return bounded.length === 0 ? undefined : bounded; } -function taskEvent(taskId: string, payload: Record): DriverEventInput { - const event: DriverEventInput = { +export function claudeBackgroundTasksClosedEvent(): DriverEventInput { + return { delivery: "lossless", - kind: "agent.task.updated", - payload: { taskId, ...payload }, + kind: "agent.tasks.replaced", + payload: { tasks: [] }, + visibility: "participant", }; - assertClaudeDurableEventFits(event, "claude.task_update_too_large", "task update"); - return event; } -export class ClaudeAgentSdkTaskEvents { - #backgroundTasks = new Map(); - - reset(): void { - this.#backgroundTasks.clear(); +export function projectClaudeBackgroundTasksSnapshot( + message: SDKBackgroundTasksChangedMessage, +): ClaudeBackgroundTasksProjection { + if (message.tasks.length > MAX_CLAUDE_BACKGROUND_TASK_ENTRIES) { + return rejectedTaskSnapshot("claude.background_tasks_snapshot_too_large", message.tasks.length); } - prepare(message: SDKBackgroundTasksChangedMessage): ClaudeTaskProjection { - if (message.tasks.length > MAX_CLAUDE_BACKGROUND_TASK_ENTRIES) { - return rejectedTaskSnapshot( - "claude.background_tasks_snapshot_too_large", - message.tasks.length, - ); + const tasks = new Map< + string, + { readonly taskId: string; readonly taskType?: string; readonly title?: string } + >(); + for (const task of message.tasks) { + if (task.ambient === true) { + continue; } - const next = new Map(); - for (const task of message.tasks) { - if (task.ambient === true) { - continue; - } - if (next.size >= MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS) { - return rejectedTaskSnapshot( - "claude.visible_background_tasks_too_many", - message.tasks.length, - ); - } - const taskId = publicTaskId(task.task_id); - if (taskId !== null) { - next.set(taskId, { - taskType: boundedTaskText(task.task_type), - title: boundedTaskText(task.description), - }); - } + const taskId = publicTaskId(task.task_id); + if (taskId === null) { + continue; } - const changes: Array<{ - readonly optimistic: boolean; - readonly task: ClaudeBackgroundTask | null; - readonly taskId: string; - }> = []; - const events: DriverEventInput[] = []; - const working = new Map(this.#backgroundTasks); - const add = ( - event: DriverEventInput, - taskId: string, - task: ClaudeBackgroundTask | null, - optimistic = false, - ): void => { - events.push(event); - changes.push({ optimistic, task, taskId }); - }; - - for (const taskId of this.#backgroundTasks.keys()) { - if (!next.has(taskId)) { - working.delete(taskId); - add(taskEvent(taskId, { active: false }), taskId, null); - } + const taskType = boundedTaskText(task.task_type); + const title = boundedTaskText(task.description); + tasks.set(taskId, { + taskId, + ...(taskType === undefined ? {} : { taskType }), + ...(title === undefined ? {} : { title }), + }); + if (tasks.size > MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS) { + return rejectedTaskSnapshot("claude.visible_background_tasks_too_many", message.tasks.length); } + } - for (const [taskId, task] of next) { - const previous = working.get(taskId); - const normalizedTask = { - taskType: task.taskType ?? previous?.taskType, - title: task.title ?? previous?.title, - }; - next.set(taskId, normalizedTask); - if (previous === undefined) { - working.set(taskId, normalizedTask); - add( - taskEvent(taskId, { - active: true, - ...(normalizedTask.taskType === undefined ? {} : { taskType: normalizedTask.taskType }), - ...(normalizedTask.title === undefined ? {} : { title: normalizedTask.title }), - }), - taskId, - normalizedTask, - true, - ); - continue; - } - - const patch: Record = {}; - if (normalizedTask.taskType !== undefined && previous.taskType !== normalizedTask.taskType) { - patch["taskType"] = normalizedTask.taskType; - } - if (normalizedTask.title !== undefined && previous.title !== normalizedTask.title) { - patch["title"] = normalizedTask.title; - } - if (Object.keys(patch).length > 0) { - working.set(taskId, normalizedTask); - add(taskEvent(taskId, patch), taskId, normalizedTask); - } - } + const event: DriverEventInput = { + delivery: "lossless", + kind: "agent.tasks.replaced", + payload: { tasks: [...tasks.values()] }, + visibility: "participant", + }; - return { - beforePublish: (index) => { - const change = changes[index]; - if (change?.optimistic === true && change.task !== null) { - this.#backgroundTasks.set(change.taskId, change.task); - } - }, - commit: () => { - this.#backgroundTasks = next; - }, - commitAccepted: (index) => { - const change = changes[index]; - if (change !== undefined) { - if (change.task === null) this.#backgroundTasks.delete(change.taskId); - else this.#backgroundTasks.set(change.taskId, change.task); - } + try { + assertClaudeDurableEventFits(event, "claude.tasks_snapshot_too_large", "task snapshot"); + } catch (error) { + if (!(error instanceof ClaudeDurableEventTooLargeError)) { + throw error; + } + const membership: DriverEventInput = { + ...event, + payload: { + tasks: [...tasks.values()].map(({ taskId }) => ({ taskId })), }, - events, }; - } - - prepareClosure(): ClaudeTaskProjection { - const taskIds = [...this.#backgroundTasks.keys()]; - const events = taskIds.map((taskId) => taskEvent(taskId, { active: false })); - + assertClaudeDurableEventFits( + membership, + "claude.tasks_membership_snapshot_too_large", + "task membership snapshot", + ); return { - beforePublish: () => {}, - commit: () => { - this.#backgroundTasks.clear(); - }, - commitAccepted: (index) => { - const taskId = taskIds[index]; - if (taskId !== undefined) this.#backgroundTasks.delete(taskId); - }, - events, + diagnostic: taskSnapshotDiagnostic("claude.tasks_snapshot_too_large", message.tasks.length), + snapshot: membership, }; } + + return { snapshot: event }; } diff --git a/src/runtimes/openai/app-server-agent-task-events.ts b/src/runtimes/openai/app-server-agent-task-events.ts new file mode 100644 index 0000000..41017e3 --- /dev/null +++ b/src/runtimes/openai/app-server-agent-task-events.ts @@ -0,0 +1,160 @@ +import { createHash } from "node:crypto"; + +import type { DriverEventInput } from "../../protocol/events"; +import { + assertOpenAiDurableEventFits, + MAX_OPENAI_DURABLE_EVENT_BYTES, +} from "./app-server-event-state"; + +const MAX_OPENAI_ACTIVE_AGENT_TASKS = 1_024; +const MAX_OPENAI_CLOSED_AGENT_TASKS = 1_024; +const MAX_OPENAI_VISIBLE_AGENT_TASKS = 256; +const MAX_OPENAI_AGENT_TASK_ID_BYTES = 256; +const MAX_OPENAI_AGENT_TASK_TEXT_LENGTH = 4_096; +const OPENAI_AGENT_TASK_EVENT_ENVELOPE_RESERVE_BYTES = 1_024; + +export interface OpenAiSubAgentActivity { + readonly agentId: string; + readonly agentPath: string; + readonly kind: "completed" | "interacted" | "interrupted" | "started"; +} + +interface OpenAiAgentTask { + readonly taskId: string; + readonly taskType?: string; + readonly title?: string; +} + +interface OpenAiAgentTaskUpdate { + readonly commit: () => void; + readonly events: DriverEventInput[]; +} + +function publicTaskId(nativeTaskId: string): string { + return Buffer.byteLength(nativeTaskId, "utf8") <= MAX_OPENAI_AGENT_TASK_ID_BYTES + ? nativeTaskId + : `openai-task:${createHash("sha256").update(nativeTaskId).digest("hex")}`; +} + +function boundedTaskText(value: string): string | undefined { + const text = value.slice(0, MAX_OPENAI_AGENT_TASK_TEXT_LENGTH); + const finalCodeUnit = text.charCodeAt(text.length - 1); + const bounded = finalCodeUnit >= 0xd800 && finalCodeUnit <= 0xdbff ? text.slice(0, -1) : text; + return bounded.length === 0 ? undefined : bounded; +} + +function taskSnapshot(tasks: readonly OpenAiAgentTask[]): DriverEventInput { + return { + delivery: "lossless", + kind: "agent.tasks.replaced", + payload: { tasks }, + visibility: "participant", + }; +} + +function taskSnapshotDiagnostic( + code: string, + taskCount: number, + message: string, +): DriverEventInput { + const event: DriverEventInput = { + delivery: "best_effort", + kind: "diagnostic.reported", + payload: { + code, + details: { taskCount }, + message, + severity: "warn", + source: "openai", + }, + visibility: "owner_debug", + }; + assertOpenAiDurableEventFits(event, "sub-agent task snapshot diagnostic"); + return event; +} + +export function openAiAgentTasksClosedEvent(): DriverEventInput { + return taskSnapshot([]); +} + +export class OpenAiAgentTaskState { + #closedTaskIds = new Set(); + #tasks = new Map(); + + prepare(activity: OpenAiSubAgentActivity): OpenAiAgentTaskUpdate { + const closedTaskIds = new Set(this.#closedTaskIds); + const tasks = new Map(this.#tasks); + const taskId = publicTaskId(activity.agentId); + + if (activity.kind === "interacted") { + closedTaskIds.delete(taskId); + this.#upsertTask(tasks, taskId, activity.agentPath); + } else if (activity.kind === "started") { + if (!closedTaskIds.has(taskId)) { + this.#upsertTask(tasks, taskId, activity.agentPath); + } + } else { + tasks.delete(taskId); + if (!closedTaskIds.has(taskId) && closedTaskIds.size === MAX_OPENAI_CLOSED_AGENT_TASKS) { + throw new RangeError("OpenAI closed sub-agent count exceeds 1024."); + } + closedTaskIds.add(taskId); + } + + let snapshot = taskSnapshot([...tasks.values()]); + const events: DriverEventInput[] = []; + + if (tasks.size > MAX_OPENAI_VISIBLE_AGENT_TASKS) { + events.push( + taskSnapshotDiagnostic( + "openai.visible_agent_tasks_too_many", + tasks.size, + "OpenAI active sub-agent count exceeded the supported snapshot size.", + ), + ); + } else if ( + Buffer.byteLength(JSON.stringify(snapshot), "utf8") > + MAX_OPENAI_DURABLE_EVENT_BYTES - OPENAI_AGENT_TASK_EVENT_ENVELOPE_RESERVE_BYTES + ) { + snapshot = taskSnapshot([...tasks.values()].map(({ taskId: id }) => ({ taskId: id }))); + assertOpenAiDurableEventFits(snapshot, "sub-agent task membership snapshot"); + events.push( + taskSnapshotDiagnostic( + "openai.agent_tasks_snapshot_too_large", + tasks.size, + "OpenAI sub-agent task metadata exceeded the supported snapshot size.", + ), + snapshot, + ); + } else { + assertOpenAiDurableEventFits(snapshot, "sub-agent task snapshot"); + events.push(snapshot); + } + + return { + commit: () => { + this.#closedTaskIds = closedTaskIds; + this.#tasks = tasks; + }, + events, + }; + } + + reset(): void { + this.#closedTaskIds.clear(); + this.#tasks.clear(); + } + + #upsertTask(tasks: Map, taskId: string, agentPath: string): void { + if (!tasks.has(taskId) && tasks.size === MAX_OPENAI_ACTIVE_AGENT_TASKS) { + throw new RangeError("OpenAI active sub-agent count exceeds 1024."); + } + + const title = boundedTaskText(agentPath); + tasks.set(taskId, { + taskId, + taskType: "openai_subagent", + ...(title === undefined ? {} : { title }), + }); + } +} diff --git a/src/runtimes/openai/app-server-driver-backend.ts b/src/runtimes/openai/app-server-driver-backend.ts index f148671..ef16580 100644 --- a/src/runtimes/openai/app-server-driver-backend.ts +++ b/src/runtimes/openai/app-server-driver-backend.ts @@ -23,6 +23,7 @@ import { writeSkillBootstrapArtifacts, } from "../skill-bootstrap"; import { OpenAiAppServerClient } from "./app-server-client"; +import { openAiAgentTasksClosedEvent } from "./app-server-agent-task-events"; import { MOSOO_OPENAI_RUNTIME_SANDBOX_MODE } from "./app-server-env"; import { OpenAiAppServerEventBridge } from "./app-server-event-bridge"; import { toOpenAiProtocolError } from "./app-server-event-mapping"; @@ -241,6 +242,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { [, bootstrapArtifacts] = await Promise.all([clientStartPromise, skillBootstrapPromise]); } catch (error) { await this.#cleanupFailedClient(context, client, "driver.openai.startup.cleanup.failed"); + await this.#publishAgentTasksClosed(context, "driver.openai.startup.failed"); signal.throwIfAborted(); throw error; } @@ -262,10 +264,14 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { ); } catch (error) { await this.#cleanupFailedClient(context, client, "driver.openai.startup.cleanup.failed"); + await this.#publishAgentTasksClosed(context, "driver.openai.startup.failed"); signal.throwIfAborted(); throw error; } - signal.throwIfAborted(); + if (signal.aborted) { + await this.#publishAgentTasksClosed(context, "driver.openai.startup.cancelled"); + signal.throwIfAborted(); + } this.#threadId = threadResult.thread.id; void this.#emitStartupTiming(context, startupStartedAt, startupPhases); @@ -331,6 +337,25 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { } } + async #publishAgentTasksClosed( + context: AgentDriverContext, + reason: string, + runId = context.ports.eventSink.currentRunId(), + ): Promise { + if (runId === null) { + return; + } + + await this.#eventPublisher.push(context, reason, [ + { + ...openAiAgentTasksClosedEvent(), + runId, + sourceEventId: `${reason}:${runId}`, + }, + ]); + this.#events.releaseTurnState(); + } + async #startThread( context: AgentDriverContext, client: OpenAiAppServerClient, @@ -634,11 +659,20 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { ...toOpenAiProtocolError({ message: error.message }), code: "openai.provider_failed", } as const; + const [taskClosure, ...itemClosures] = this.#events.turnStartTerminalEvents({ + error: protocolError, + kind: "failed", + }); await this.#eventPublisher.pushTerminal( context, "driver.openai.provider.failed", [ + { + ...taskClosure, + runId, + sourceEventId: `openai.provider.failed.closure:${runId}:0`, + }, { kind: "run.started", payload: { @@ -647,14 +681,12 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { runId, sourceEventId: `openai.provider.failed.started:${runId}`, }, - ...this.#events - .turnStartTerminalEvents({ error: protocolError, kind: "failed" }) - .map((event, index) => ({ - ...event, - runId, - sourceEventId: - event.sourceEventId ?? `openai.provider.failed.closure:${runId}:${String(index)}`, - })), + ...itemClosures.map((event, index) => ({ + ...event, + runId, + sourceEventId: + event.sourceEventId ?? `openai.provider.failed.closure:${runId}:${String(index + 1)}`, + })), ], { kind: "run.failed", @@ -676,10 +708,18 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { runId: RunId, reason: string, ): Promise { + const [taskClosure, ...itemClosures] = this.#events.turnStartTerminalEvents({ + kind: "cancelled", + }); await this.#eventPublisher.pushTerminal( context, "driver.openai.turn_start.cancelled", [ + { + ...taskClosure, + runId, + sourceEventId: `openai.turn_start.cancelled.closure:${runId}:0`, + }, { kind: "run.started", payload: { @@ -698,11 +738,12 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { runId, sourceEventId: `openai.turn_start.cancelled.requested:${runId}`, }, - ...this.#events.turnStartTerminalEvents({ kind: "cancelled" }).map((event, index) => ({ + ...itemClosures.map((event, index) => ({ ...event, runId, sourceEventId: - event.sourceEventId ?? `openai.turn_start.cancelled.closure:${runId}:${String(index)}`, + event.sourceEventId ?? + `openai.turn_start.cancelled.closure:${runId}:${String(index + 1)}`, })), ], { @@ -801,6 +842,13 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { void serverRequests.catch(() => {}); if (this.#turnStartInFlight) { + if (!publishTurnStartCancellation) { + await this.#publishAgentTasksClosed( + context, + "driver.openai.turn_start.stopped", + this.#turnStartRunId, + ); + } this.#pendingTurnStartCancellationEvent = publishTurnStartCancellation; this.#pendingTurnStartCancellationReason = reason; this.#pendingTurnStartServerRequests = serverRequests; diff --git a/src/runtimes/openai/app-server-event-bridge.ts b/src/runtimes/openai/app-server-event-bridge.ts index 62d28d3..b151811 100644 --- a/src/runtimes/openai/app-server-event-bridge.ts +++ b/src/runtimes/openai/app-server-event-bridge.ts @@ -210,6 +210,7 @@ export class OpenAiAppServerEventBridge { return; } const publicTurnId = this.publicTurnId(turnId); + const [taskClosure, ...itemClosures] = this.#itemEvents.terminalEvents({ kind: "cancelled" }); try { await this.#options.pushTerminal( @@ -218,6 +219,7 @@ export class OpenAiAppServerEventBridge { turnClosureEvents({ eventName: "turn.cancelled", events: [ + taskClosure, { ...turnEventFields({ eventName: "turn.cancel.requested", publicTurnId, runId }), kind: "run.cancel.requested", @@ -227,7 +229,7 @@ export class OpenAiAppServerEventBridge { targetRunId: runId, }, }, - ...this.#itemEvents.terminalEvents({ kind: "cancelled" }), + ...itemClosures, ], publicTurnId, runId, @@ -310,7 +312,9 @@ export class OpenAiAppServerEventBridge { this.#itemEvents.reset(); } - turnStartTerminalEvents(outcome: OpenAiTerminalOutcome): DriverEventInput[] { + turnStartTerminalEvents( + outcome: OpenAiTerminalOutcome, + ): [DriverEventInput, ...DriverEventInput[]] { return this.#itemEvents.terminalEvents(outcome); } diff --git a/src/runtimes/openai/app-server-item-events.ts b/src/runtimes/openai/app-server-item-events.ts index b0a4163..b60cc5b 100644 --- a/src/runtimes/openai/app-server-item-events.ts +++ b/src/runtimes/openai/app-server-item-events.ts @@ -16,6 +16,11 @@ import type { } from "./app-server-event-state"; import { isRecord, readArray, readNonEmptyString, readRecord, readString } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; +import { + OpenAiAgentTaskState, + openAiAgentTasksClosedEvent, + type OpenAiSubAgentActivity, +} from "./app-server-agent-task-events"; import { toOpenAiCollaborationOutput, toOpenAiFileChangeEvents, @@ -100,6 +105,26 @@ interface OpenAiItemCompletionCommit { readonly events: DriverEventInput[]; } +function readOpenAiSubAgentActivity(item: JsonObject): OpenAiSubAgentActivity | null { + if (readString(item, "type") !== "subAgentActivity") { + return null; + } + + const kind = readString(item, "kind"); + const agentId = readNonEmptyString(item, "agentThreadId"); + const agentPath = readNonEmptyString(item, "agentPath"); + + if ( + agentId === null || + agentPath === null || + (kind !== "started" && kind !== "interacted" && kind !== "interrupted" && kind !== "completed") + ) { + throw new Error("OpenAI sub-agent activity is malformed."); + } + + return { agentId, agentPath, kind }; +} + function withOpenAiEventIds( events: readonly DriverEventInput[], sourcePrefix: string, @@ -143,20 +168,7 @@ function toOpenAiItemLifecycleEvents( return []; } - const activityKind = readString(item, "kind"); - const agentId = readNonEmptyString(item, "agentThreadId"); - const agentPath = readNonEmptyString(item, "agentPath"); - - if ( - agentId === null || - agentPath === null || - (activityKind !== "started" && - activityKind !== "interacted" && - activityKind !== "interrupted" && - activityKind !== "completed") - ) { - throw new Error("OpenAI sub-agent activity is malformed."); - } + const { agentId, agentPath, kind: activityKind } = readOpenAiSubAgentActivity(item)!; return [ { @@ -323,6 +335,7 @@ function toOpenAiToolStatus( } export class OpenAiAppServerItemEventBridge { + readonly #agentTasks = new OpenAiAgentTaskState(); readonly #citationDiagnosticsEmitted = new Set(); readonly #citationFilters = new Map(); readonly #items: OpenAiItemState; @@ -349,6 +362,7 @@ export class OpenAiAppServerItemEventBridge { } reset(): void { + this.#agentTasks.reset(); this.#citationDiagnosticsEmitted.clear(); this.#citationFilters.clear(); this.#items.reset(); @@ -357,8 +371,12 @@ export class OpenAiAppServerItemEventBridge { this.#tools.reset(); } - terminalEvents(outcome: OpenAiTerminalOutcome): DriverEventInput[] { - return [...this.#messages.terminalEvents(outcome), ...this.#tools.terminalEvents(outcome)]; + terminalEvents(outcome: OpenAiTerminalOutcome): [DriverEventInput, ...DriverEventInput[]] { + return [ + openAiAgentTasksClosedEvent(), + ...this.#messages.terminalEvents(outcome), + ...this.#tools.terminalEvents(outcome), + ]; } async onMessageDelta(context: AgentDriverContext, params: JsonObject): Promise { @@ -454,6 +472,8 @@ export class OpenAiAppServerItemEventBridge { // Validate and prepare the full completion before committing replay state. const toolStatus = toOpenAiToolStatus(item, "completed"); + const activity = readOpenAiSubAgentActivity(item); + const agentTaskUpdate = activity === null ? null : this.#agentTasks.prepare(activity); const publicItemId = this.#items.publicId(itemId); const publicTurnId = this.#items.publicId(turnId, "turn"); const lifecycleEvents = toOpenAiItemLifecycleEvents(item, publicItemId, "completed"); @@ -467,6 +487,7 @@ export class OpenAiAppServerItemEventBridge { events.push(...toOpenAiFileChangeEvents(item)); events.push(...lifecycleEvents); + events.push(...(agentTaskUpdate?.events ?? [])); if (events.length > 0) { const durableEvents = withOpenAiEventIds( @@ -482,6 +503,7 @@ export class OpenAiAppServerItemEventBridge { for (const completion of completions) { completion.commit(); } + agentTaskUpdate?.commit(); this.#items.markCompleted(itemId); } diff --git a/src/runtimes/openai/app-server-protocol-types.ts b/src/runtimes/openai/app-server-protocol-types.ts index d92e77f..73f1d26 100644 --- a/src/runtimes/openai/app-server-protocol-types.ts +++ b/src/runtimes/openai/app-server-protocol-types.ts @@ -1,32 +1,26 @@ -import type { - ClientRequest as GeneratedClientRequest, - RequestId as GeneratedRequestId, - ServerNotification as GeneratedServerNotification, - ServerRequest as GeneratedServerRequest, -} from "./generated"; -import type { - AskForApproval, - CurrentTimeReadResponse as GeneratedCurrentTimeReadResponse, - ThreadInjectItemsParams as GeneratedThreadInjectItemsParams, - ThreadResumeParams as GeneratedThreadResumeParams, - ThreadStartParams as GeneratedThreadStartParams, - TurnStartParams as GeneratedTurnStartParams, - TurnStatus as GeneratedTurnStatus, -} from "./generated/v2"; +import type { InitializeParams } from "./generated/InitializeParams"; +import type { ServerNotificationMethod, ServerRequestMethod } from "./generated/ProtocolMethods"; +import type { RequestId } from "./generated/RequestId"; +import type { ThreadBackgroundTerminalsCleanParams } from "./generated/v2/ThreadBackgroundTerminalsCleanParams"; +import type { ThreadInjectItemsParams } from "./generated/v2/ThreadInjectItemsParams"; +import type { ThreadResumeParams } from "./generated/v2/ThreadResumeParams"; +import type { ThreadStartParams } from "./generated/v2/ThreadStartParams"; +import type { TurnStartParams } from "./generated/v2/TurnStartParams"; +import type { TurnStatus } from "./generated/v2/TurnStatus"; import type { JsonObject } from "./app-server-json"; -type WireServerNotification = Exclude< - GeneratedServerNotification, - { method: "rawResponse/completed" | "rawResponseItem/completed" } ->; - -export type RequestId = GeneratedRequestId; -export type ApprovalPolicy = AskForApproval; -export type ThreadStartParams = GeneratedThreadStartParams; -export type ThreadResumeParams = GeneratedThreadResumeParams; -export type ThreadInjectItemsParams = GeneratedThreadInjectItemsParams; -export type TurnStartParams = GeneratedTurnStartParams; -export type TurnStatus = GeneratedTurnStatus; +export type { AskForApproval as ApprovalPolicy } from "./generated/v2/AskForApproval"; +export type { CurrentTimeReadResponse } from "./generated/v2/CurrentTimeReadResponse"; +export type { + RequestId, + ServerNotificationMethod, + ServerRequestMethod, + ThreadInjectItemsParams, + ThreadResumeParams, + ThreadStartParams, + TurnStartParams, + TurnStatus, +}; export type InitializeResponse = JsonObject; export type ThreadStartResponse = JsonObject & { thread: JsonObject & { id: string } }; @@ -48,16 +42,15 @@ export interface ClientRequestResult { export type ClientRequestMethod = keyof ClientRequestResult; -type ClientRequestParamsFor = - Extract extends { params: infer Params } - ? Params - : never; - -export type ClientRequestParams = { - [Method in ClientRequestMethod]: ClientRequestParamsFor; -}; +export interface ClientRequestParams { + initialize: InitializeParams; + "thread/backgroundTerminals/clean": ThreadBackgroundTerminalsCleanParams; + "thread/inject_items": ThreadInjectItemsParams; + "thread/resume": ThreadResumeParams; + "thread/start": ThreadStartParams; + "turn/start": TurnStartParams; +} -export type ServerNotificationMethod = WireServerNotification["method"]; export type ServerNotificationParams = { [Method in ServerNotificationMethod]: JsonObject; }; @@ -68,8 +61,6 @@ export interface ParsedServerNotification { params: JsonObject; } -export type ServerRequestMethod = GeneratedServerRequest["method"]; - export interface ParsedServerRequest { id: RequestId; method: ServerRequestMethod; @@ -81,5 +72,3 @@ export interface PermissionsRequestApprovalResponse { scope: "turn" | "session"; strictAutoReview?: boolean; } - -export type CurrentTimeReadResponse = GeneratedCurrentTimeReadResponse; diff --git a/src/runtimes/openai/auth-state.ts b/src/runtimes/openai/auth-state.ts index a83a777..4f545d6 100644 --- a/src/runtimes/openai/auth-state.ts +++ b/src/runtimes/openai/auth-state.ts @@ -1,7 +1,6 @@ import { join } from "node:path"; import type { JsonObject, JsonValue } from "../../protocol/json"; -import { isJsonObject } from "../../protocol/json"; import { writeFileAtomicallyAtPath } from "../atomic-file"; import { mergeProviderOptions } from "../provider-options"; interface OpenAiApiKeyAuthStateInput { @@ -45,78 +44,6 @@ function readEnvVar(env: NodeJS.ProcessEnv, key: string): string | null { return value || null; } -function toTomlString(value: string): string { - return JSON.stringify(value); -} - -function toTomlKeySegment(value: string): string { - return /^[A-Za-z0-9_-]+$/.test(value) ? value : toTomlString(value); -} - -function toTomlInlineValue(value: JsonValue, path: string): string { - if (value === null) { - throw new Error(`OpenAI provider option ${path} cannot be null in config.toml.`); - } - - if (typeof value === "string") { - return toTomlString(value); - } - - if (typeof value === "number" || typeof value === "boolean") { - return String(value); - } - - if (Array.isArray(value)) { - return `[${value.map((entry, index) => toTomlInlineValue(entry, `${path}[${index}]`)).join(", ")}]`; - } - - return `{ ${Object.entries(value) - .map( - ([key, entry]) => `${toTomlKeySegment(key)} = ${toTomlInlineValue(entry, `${path}.${key}`)}`, - ) - .join(", ")} }`; -} - -function isTomlTable(value: unknown): value is JsonObject { - return isJsonObject(value); -} - -function appendTomlObject( - lines: string[], - object: Record, - path: string[] = [], -): void { - if (path.length > 0) { - if (lines.length > 0 && lines.at(-1) !== "") { - lines.push(""); - } - - lines.push(`[${path.map(toTomlKeySegment).join(".")}]`); - } - - const nestedEntries: [string, JsonObject][] = []; - - for (const [key, value] of Object.entries(object)) { - if (isTomlTable(value)) { - nestedEntries.push([key, value]); - } else { - lines.push( - `${toTomlKeySegment(key)} = ${toTomlInlineValue(value, [...path, key].join("."))}`, - ); - } - } - - for (const [key, value] of nestedEntries) { - appendTomlObject(lines, value, [...path, key]); - } -} - -function stringifyToml(object: Record): string { - const lines: string[] = []; - appendTomlObject(lines, object); - return `${lines.join("\n")}\n`; -} - export async function materializeOpenAiApiKeyAuthState( input: OpenAiApiKeyAuthStateInput, ): Promise { @@ -194,7 +121,7 @@ export async function materializeOpenAiModelProviderConfig( const config = mergeProviderOptions(generatedConfig, input.providerOptions ?? {}); - const written = await writeFileAtomicallyAtPath(configTomlPath, stringifyToml(config), { + const written = await writeFileAtomicallyAtPath(configTomlPath, Bun.TOML.stringify(config)!, { mode: 0o666, skipIfUnchanged: true, }); diff --git a/src/runtimes/openai/generated-json-schema/README.md b/src/runtimes/openai/generated-json-schema/README.md index 5480a44..ff0d31d 100644 --- a/src/runtimes/openai/generated-json-schema/README.md +++ b/src/runtimes/openai/generated-json-schema/README.md @@ -3,6 +3,7 @@ These eight runtime schemas are selected from `@openai/codex@0.150.1` output and must not be edited by hand. ```sh +set -eu vp exec codex --version schema_tmp=$(mktemp -d) trap 'rm -rf "$schema_tmp"' EXIT diff --git a/src/runtimes/openai/generated/AgentPath.ts b/src/runtimes/openai/generated/AgentPath.ts deleted file mode 100644 index 6e55ce6..0000000 --- a/src/runtimes/openai/generated/AgentPath.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AgentPath = string; diff --git a/src/runtimes/openai/generated/ApplyPatchApprovalParams.ts b/src/runtimes/openai/generated/ApplyPatchApprovalParams.ts deleted file mode 100644 index ab04f9f..0000000 --- a/src/runtimes/openai/generated/ApplyPatchApprovalParams.ts +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileChange } from "./FileChange"; -import type { ThreadId } from "./ThreadId"; - -export type ApplyPatchApprovalParams = { - conversationId: ThreadId; - /** - * Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent] - * and [codex_protocol::protocol::PatchApplyEndEvent]. - */ - callId: string; - fileChanges: { [key in string]?: FileChange }; - /** - * Optional explanatory reason (e.g. request for extra write access). - */ - reason: string | null; - /** - * When set, the agent is asking the user to allow writes under this root - * for the remainder of the session (unclear if this is honored today). - */ - grantRoot: string | null; -}; diff --git a/src/runtimes/openai/generated/ApplyPatchApprovalResponse.ts b/src/runtimes/openai/generated/ApplyPatchApprovalResponse.ts deleted file mode 100644 index f8ebead..0000000 --- a/src/runtimes/openai/generated/ApplyPatchApprovalResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReviewDecision } from "./ReviewDecision"; - -export type ApplyPatchApprovalResponse = { decision: ReviewDecision }; diff --git a/src/runtimes/openai/generated/AuthMode.ts b/src/runtimes/openai/generated/AuthMode.ts deleted file mode 100644 index 820d826..0000000 --- a/src/runtimes/openai/generated/AuthMode.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Authentication mode for OpenAI-backed providers. - */ -export type AuthMode = - | "apikey" - | "chatgpt" - | "chatgptAuthTokens" - | "headers" - | "agentIdentity" - | "personalAccessToken" - | "bedrockApiKey" - | "bedrockAccessKeys"; diff --git a/src/runtimes/openai/generated/AutoCompactTokenLimitScope.ts b/src/runtimes/openai/generated/AutoCompactTokenLimitScope.ts deleted file mode 100644 index 3d9a56c..0000000 --- a/src/runtimes/openai/generated/AutoCompactTokenLimitScope.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Selects which part of the active context is charged against - * `model_auto_compact_token_limit`. - */ -export type AutoCompactTokenLimitScope = "total" | "body_after_prefix"; diff --git a/src/runtimes/openai/generated/ClientNotification.ts b/src/runtimes/openai/generated/ClientNotification.ts deleted file mode 100644 index 814c9de..0000000 --- a/src/runtimes/openai/generated/ClientNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ClientNotification = { method: "initialized" }; diff --git a/src/runtimes/openai/generated/ClientRequest.ts b/src/runtimes/openai/generated/ClientRequest.ts deleted file mode 100644 index 2da145b..0000000 --- a/src/runtimes/openai/generated/ClientRequest.ts +++ /dev/null @@ -1,392 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams"; -import type { FuzzyFileSearchSessionStartParams } from "./FuzzyFileSearchSessionStartParams"; -import type { FuzzyFileSearchSessionStopParams } from "./FuzzyFileSearchSessionStopParams"; -import type { FuzzyFileSearchSessionUpdateParams } from "./FuzzyFileSearchSessionUpdateParams"; -import type { GetAuthStatusParams } from "./GetAuthStatusParams"; -import type { GetConversationSummaryParams } from "./GetConversationSummaryParams"; -import type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams"; -import type { InitializeParams } from "./InitializeParams"; -import type { RequestId } from "./RequestId"; -import type { AppsInstalledParams } from "./v2/AppsInstalledParams"; -import type { AppsListParams } from "./v2/AppsListParams"; -import type { AppsReadParams } from "./v2/AppsReadParams"; -import type { BedrockDiscoverParams } from "./v2/BedrockDiscoverParams"; -import type { BedrockSetupParams } from "./v2/BedrockSetupParams"; -import type { CancelLoginAccountParams } from "./v2/CancelLoginAccountParams"; -import type { CollaborationModeListParams } from "./v2/CollaborationModeListParams"; -import type { CommandExecParams } from "./v2/CommandExecParams"; -import type { CommandExecResizeParams } from "./v2/CommandExecResizeParams"; -import type { CommandExecTerminateParams } from "./v2/CommandExecTerminateParams"; -import type { CommandExecWriteParams } from "./v2/CommandExecWriteParams"; -import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams"; -import type { ConfigReadParams } from "./v2/ConfigReadParams"; -import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams"; -import type { ConsumeAccountRateLimitResetCreditParams } from "./v2/ConsumeAccountRateLimitResetCreditParams"; -import type { EnvironmentAddParams } from "./v2/EnvironmentAddParams"; -import type { EnvironmentInfoParams } from "./v2/EnvironmentInfoParams"; -import type { EnvironmentStatusParams } from "./v2/EnvironmentStatusParams"; -import type { ExperimentalFeatureEnablementSetParams } from "./v2/ExperimentalFeatureEnablementSetParams"; -import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureListParams"; -import type { ExternalAgentConfigDetectParams } from "./v2/ExternalAgentConfigDetectParams"; -import type { ExternalAgentConfigImportHistoryRecordParams } from "./v2/ExternalAgentConfigImportHistoryRecordParams"; -import type { ExternalAgentConfigImportParams } from "./v2/ExternalAgentConfigImportParams"; -import type { FeedbackUploadParams } from "./v2/FeedbackUploadParams"; -import type { FsCopyParams } from "./v2/FsCopyParams"; -import type { FsCreateDirectoryParams } from "./v2/FsCreateDirectoryParams"; -import type { FsGetMetadataParams } from "./v2/FsGetMetadataParams"; -import type { FsReadDirectoryParams } from "./v2/FsReadDirectoryParams"; -import type { FsReadFileParams } from "./v2/FsReadFileParams"; -import type { FsRemoveParams } from "./v2/FsRemoveParams"; -import type { FsUnwatchParams } from "./v2/FsUnwatchParams"; -import type { FsWatchParams } from "./v2/FsWatchParams"; -import type { FsWriteFileParams } from "./v2/FsWriteFileParams"; -import type { GetAccountParams } from "./v2/GetAccountParams"; -import type { GetAccountTokenUsageParams } from "./v2/GetAccountTokenUsageParams"; -import type { HooksListParams } from "./v2/HooksListParams"; -import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; -import type { LoginAccountParams } from "./v2/LoginAccountParams"; -import type { MarketplaceAddParams } from "./v2/MarketplaceAddParams"; -import type { MarketplaceRemoveParams } from "./v2/MarketplaceRemoveParams"; -import type { MarketplaceUpgradeParams } from "./v2/MarketplaceUpgradeParams"; -import type { McpResourceReadParams } from "./v2/McpResourceReadParams"; -import type { McpServerEventStreamStartParams } from "./v2/McpServerEventStreamStartParams"; -import type { McpServerEventStreamStopParams } from "./v2/McpServerEventStreamStopParams"; -import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; -import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams"; -import type { MockExperimentalMethodParams } from "./v2/MockExperimentalMethodParams"; -import type { ModelListParams } from "./v2/ModelListParams"; -import type { ModelProviderCapabilitiesReadParams } from "./v2/ModelProviderCapabilitiesReadParams"; -import type { PermissionProfileListParams } from "./v2/PermissionProfileListParams"; -import type { PluginInstallParams } from "./v2/PluginInstallParams"; -import type { PluginInstalledParams } from "./v2/PluginInstalledParams"; -import type { PluginListParams } from "./v2/PluginListParams"; -import type { PluginReadParams } from "./v2/PluginReadParams"; -import type { PluginSearchParams } from "./v2/PluginSearchParams"; -import type { PluginShareCheckoutParams } from "./v2/PluginShareCheckoutParams"; -import type { PluginShareDeleteParams } from "./v2/PluginShareDeleteParams"; -import type { PluginShareListParams } from "./v2/PluginShareListParams"; -import type { PluginShareSaveParams } from "./v2/PluginShareSaveParams"; -import type { PluginShareUpdateTargetsParams } from "./v2/PluginShareUpdateTargetsParams"; -import type { PluginSkillReadParams } from "./v2/PluginSkillReadParams"; -import type { PluginUninstallParams } from "./v2/PluginUninstallParams"; -import type { ProcessKillParams } from "./v2/ProcessKillParams"; -import type { ProcessResizePtyParams } from "./v2/ProcessResizePtyParams"; -import type { ProcessSpawnParams } from "./v2/ProcessSpawnParams"; -import type { ProcessWriteStdinParams } from "./v2/ProcessWriteStdinParams"; -import type { ProjectCreateParams } from "./v2/ProjectCreateParams"; -import type { ProjectDeleteParams } from "./v2/ProjectDeleteParams"; -import type { ProjectImportParams } from "./v2/ProjectImportParams"; -import type { ProjectListParams } from "./v2/ProjectListParams"; -import type { ProjectMoveParams } from "./v2/ProjectMoveParams"; -import type { ProjectReadParams } from "./v2/ProjectReadParams"; -import type { ProjectUpdateParams } from "./v2/ProjectUpdateParams"; -import type { RemoteControlClientsListParams } from "./v2/RemoteControlClientsListParams"; -import type { RemoteControlClientsRevokeParams } from "./v2/RemoteControlClientsRevokeParams"; -import type { RemoteControlDisableParams } from "./v2/RemoteControlDisableParams"; -import type { RemoteControlEnableParams } from "./v2/RemoteControlEnableParams"; -import type { RemoteControlPairingStartParams } from "./v2/RemoteControlPairingStartParams"; -import type { RemoteControlPairingStatusParams } from "./v2/RemoteControlPairingStatusParams"; -import type { ReviewStartParams } from "./v2/ReviewStartParams"; -import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams"; -import type { ServerDiagnosticsParams } from "./v2/ServerDiagnosticsParams"; -import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams"; -import type { SkillsExtraRootsSetParams } from "./v2/SkillsExtraRootsSetParams"; -import type { SkillsListParams } from "./v2/SkillsListParams"; -import type { ThreadApproveGuardianDeniedActionParams } from "./v2/ThreadApproveGuardianDeniedActionParams"; -import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams"; -import type { ThreadBackgroundTerminalsCleanParams } from "./v2/ThreadBackgroundTerminalsCleanParams"; -import type { ThreadBackgroundTerminalsListParams } from "./v2/ThreadBackgroundTerminalsListParams"; -import type { ThreadBackgroundTerminalsTerminateParams } from "./v2/ThreadBackgroundTerminalsTerminateParams"; -import type { ThreadCompactStartParams } from "./v2/ThreadCompactStartParams"; -import type { ThreadDecrementElicitationParams } from "./v2/ThreadDecrementElicitationParams"; -import type { ThreadDeleteParams } from "./v2/ThreadDeleteParams"; -import type { ThreadForkParams } from "./v2/ThreadForkParams"; -import type { ThreadGoalClearParams } from "./v2/ThreadGoalClearParams"; -import type { ThreadGoalGetParams } from "./v2/ThreadGoalGetParams"; -import type { ThreadGoalSetParams } from "./v2/ThreadGoalSetParams"; -import type { ThreadIncrementElicitationParams } from "./v2/ThreadIncrementElicitationParams"; -import type { ThreadInjectItemsParams } from "./v2/ThreadInjectItemsParams"; -import type { ThreadItemsListParams } from "./v2/ThreadItemsListParams"; -import type { ThreadListParams } from "./v2/ThreadListParams"; -import type { ThreadLoadedListParams } from "./v2/ThreadLoadedListParams"; -import type { ThreadMemoryModeSetParams } from "./v2/ThreadMemoryModeSetParams"; -import type { ThreadMetadataUpdateParams } from "./v2/ThreadMetadataUpdateParams"; -import type { ThreadQueueAddParams } from "./v2/ThreadQueueAddParams"; -import type { ThreadQueueDeleteParams } from "./v2/ThreadQueueDeleteParams"; -import type { ThreadQueueListParams } from "./v2/ThreadQueueListParams"; -import type { ThreadQueueReorderParams } from "./v2/ThreadQueueReorderParams"; -import type { ThreadQueueStartParams } from "./v2/ThreadQueueStartParams"; -import type { ThreadQueueUpdateParams } from "./v2/ThreadQueueUpdateParams"; -import type { ThreadReadParams } from "./v2/ThreadReadParams"; -import type { ThreadRealtimeAppendAudioParams } from "./v2/ThreadRealtimeAppendAudioParams"; -import type { ThreadRealtimeAppendSpeechParams } from "./v2/ThreadRealtimeAppendSpeechParams"; -import type { ThreadRealtimeAppendTextParams } from "./v2/ThreadRealtimeAppendTextParams"; -import type { ThreadRealtimeListVoicesParams } from "./v2/ThreadRealtimeListVoicesParams"; -import type { ThreadRealtimeStartParams } from "./v2/ThreadRealtimeStartParams"; -import type { ThreadRealtimeStopParams } from "./v2/ThreadRealtimeStopParams"; -import type { ThreadResumeParams } from "./v2/ThreadResumeParams"; -import type { ThreadRevertParams } from "./v2/ThreadRevertParams"; -import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams"; -import type { ThreadSearchOccurrencesParams } from "./v2/ThreadSearchOccurrencesParams"; -import type { ThreadSearchParams } from "./v2/ThreadSearchParams"; -import type { ThreadSectionCreateParams } from "./v2/ThreadSectionCreateParams"; -import type { ThreadSectionDeleteParams } from "./v2/ThreadSectionDeleteParams"; -import type { ThreadSectionListParams } from "./v2/ThreadSectionListParams"; -import type { ThreadSectionMoveParams } from "./v2/ThreadSectionMoveParams"; -import type { ThreadSectionUpdateParams } from "./v2/ThreadSectionUpdateParams"; -import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams"; -import type { ThreadSettingsUpdateParams } from "./v2/ThreadSettingsUpdateParams"; -import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams"; -import type { ThreadStartParams } from "./v2/ThreadStartParams"; -import type { ThreadTimelineListParams } from "./v2/ThreadTimelineListParams"; -import type { ThreadTurnsListParams } from "./v2/ThreadTurnsListParams"; -import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams"; -import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams"; -import type { TurnInterruptParams } from "./v2/TurnInterruptParams"; -import type { TurnStartParams } from "./v2/TurnStartParams"; -import type { TurnSteerParams } from "./v2/TurnSteerParams"; -import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupStartParams"; - -/** - * Request from the client to the server. - */ -export type ClientRequest = - | { method: "initialize"; id: RequestId; params: InitializeParams } - | { method: "server/diagnostics"; id: RequestId; params: ServerDiagnosticsParams } - | { method: "thread/start"; id: RequestId; params: ThreadStartParams } - | { method: "thread/resume"; id: RequestId; params: ThreadResumeParams } - | { method: "thread/fork"; id: RequestId; params: ThreadForkParams } - | { method: "thread/archive"; id: RequestId; params: ThreadArchiveParams } - | { method: "thread/delete"; id: RequestId; params: ThreadDeleteParams } - | { method: "thread/unsubscribe"; id: RequestId; params: ThreadUnsubscribeParams } - | { - method: "thread/increment_elicitation"; - id: RequestId; - params: ThreadIncrementElicitationParams; - } - | { - method: "thread/decrement_elicitation"; - id: RequestId; - params: ThreadDecrementElicitationParams; - } - | { method: "thread/name/set"; id: RequestId; params: ThreadSetNameParams } - | { method: "thread/goal/set"; id: RequestId; params: ThreadGoalSetParams } - | { method: "thread/goal/get"; id: RequestId; params: ThreadGoalGetParams } - | { method: "thread/goal/clear"; id: RequestId; params: ThreadGoalClearParams } - | { method: "thread/queue/add"; id: RequestId; params: ThreadQueueAddParams } - | { method: "thread/queue/list"; id: RequestId; params: ThreadQueueListParams } - | { method: "thread/queue/update"; id: RequestId; params: ThreadQueueUpdateParams } - | { method: "thread/queue/delete"; id: RequestId; params: ThreadQueueDeleteParams } - | { method: "thread/queue/reorder"; id: RequestId; params: ThreadQueueReorderParams } - | { method: "thread/queue/start"; id: RequestId; params: ThreadQueueStartParams } - | { method: "thread/metadata/update"; id: RequestId; params: ThreadMetadataUpdateParams } - | { method: "thread/section/move"; id: RequestId; params: ThreadSectionMoveParams } - | { method: "thread/settings/update"; id: RequestId; params: ThreadSettingsUpdateParams } - | { method: "thread/memoryMode/set"; id: RequestId; params: ThreadMemoryModeSetParams } - | { method: "memory/reset"; id: RequestId; params: undefined } - | { method: "thread/unarchive"; id: RequestId; params: ThreadUnarchiveParams } - | { method: "thread/compact/start"; id: RequestId; params: ThreadCompactStartParams } - | { method: "thread/shellCommand"; id: RequestId; params: ThreadShellCommandParams } - | { - method: "thread/approveGuardianDeniedAction"; - id: RequestId; - params: ThreadApproveGuardianDeniedActionParams; - } - | { - method: "thread/backgroundTerminals/clean"; - id: RequestId; - params: ThreadBackgroundTerminalsCleanParams; - } - | { - method: "thread/backgroundTerminals/list"; - id: RequestId; - params: ThreadBackgroundTerminalsListParams; - } - | { - method: "thread/backgroundTerminals/terminate"; - id: RequestId; - params: ThreadBackgroundTerminalsTerminateParams; - } - | { method: "thread/rollback"; id: RequestId; params: ThreadRollbackParams } - | { method: "thread/revert"; id: RequestId; params: ThreadRevertParams } - | { method: "thread/list"; id: RequestId; params: ThreadListParams } - | { method: "project/list"; id: RequestId; params: ProjectListParams } - | { method: "project/read"; id: RequestId; params: ProjectReadParams } - | { method: "project/create"; id: RequestId; params: ProjectCreateParams } - | { method: "project/import"; id: RequestId; params: ProjectImportParams } - | { method: "project/update"; id: RequestId; params: ProjectUpdateParams } - | { method: "project/move"; id: RequestId; params: ProjectMoveParams } - | { method: "project/delete"; id: RequestId; params: ProjectDeleteParams } - | { method: "threadSection/list"; id: RequestId; params: ThreadSectionListParams } - | { method: "threadSection/create"; id: RequestId; params: ThreadSectionCreateParams } - | { method: "threadSection/update"; id: RequestId; params: ThreadSectionUpdateParams } - | { method: "threadSection/delete"; id: RequestId; params: ThreadSectionDeleteParams } - | { method: "thread/search"; id: RequestId; params: ThreadSearchParams } - | { method: "thread/searchOccurrences"; id: RequestId; params: ThreadSearchOccurrencesParams } - | { method: "thread/loaded/list"; id: RequestId; params: ThreadLoadedListParams } - | { method: "thread/read"; id: RequestId; params: ThreadReadParams } - | { method: "thread/turns/list"; id: RequestId; params: ThreadTurnsListParams } - | { method: "thread/items/list"; id: RequestId; params: ThreadItemsListParams } - | { method: "thread/inject_items"; id: RequestId; params: ThreadInjectItemsParams } - | { method: "skills/list"; id: RequestId; params: SkillsListParams } - | { method: "skills/extraRoots/set"; id: RequestId; params: SkillsExtraRootsSetParams } - | { method: "hooks/list"; id: RequestId; params: HooksListParams } - | { method: "marketplace/add"; id: RequestId; params: MarketplaceAddParams } - | { method: "marketplace/remove"; id: RequestId; params: MarketplaceRemoveParams } - | { method: "marketplace/upgrade"; id: RequestId; params: MarketplaceUpgradeParams } - | { method: "plugin/list"; id: RequestId; params: PluginListParams } - | { method: "plugin/search"; id: RequestId; params: PluginSearchParams } - | { method: "plugin/installed"; id: RequestId; params: PluginInstalledParams } - | { method: "plugin/read"; id: RequestId; params: PluginReadParams } - | { method: "plugin/skill/read"; id: RequestId; params: PluginSkillReadParams } - | { method: "plugin/share/save"; id: RequestId; params: PluginShareSaveParams } - | { method: "plugin/share/updateTargets"; id: RequestId; params: PluginShareUpdateTargetsParams } - | { method: "plugin/share/list"; id: RequestId; params: PluginShareListParams } - | { method: "plugin/share/checkout"; id: RequestId; params: PluginShareCheckoutParams } - | { method: "plugin/share/delete"; id: RequestId; params: PluginShareDeleteParams } - | { method: "app/read"; id: RequestId; params: AppsReadParams } - | { method: "app/list"; id: RequestId; params: AppsListParams } - | { method: "app/installed"; id: RequestId; params: AppsInstalledParams } - | { method: "fs/readFile"; id: RequestId; params: FsReadFileParams } - | { method: "fs/writeFile"; id: RequestId; params: FsWriteFileParams } - | { method: "fs/createDirectory"; id: RequestId; params: FsCreateDirectoryParams } - | { method: "fs/getMetadata"; id: RequestId; params: FsGetMetadataParams } - | { method: "fs/readDirectory"; id: RequestId; params: FsReadDirectoryParams } - | { method: "fs/remove"; id: RequestId; params: FsRemoveParams } - | { method: "fs/copy"; id: RequestId; params: FsCopyParams } - | { method: "fs/watch"; id: RequestId; params: FsWatchParams } - | { method: "fs/unwatch"; id: RequestId; params: FsUnwatchParams } - | { method: "skills/config/write"; id: RequestId; params: SkillsConfigWriteParams } - | { method: "plugin/install"; id: RequestId; params: PluginInstallParams } - | { method: "plugin/uninstall"; id: RequestId; params: PluginUninstallParams } - | { method: "turn/start"; id: RequestId; params: TurnStartParams } - | { method: "turn/steer"; id: RequestId; params: TurnSteerParams } - | { method: "turn/interrupt"; id: RequestId; params: TurnInterruptParams } - | { method: "thread/realtime/start"; id: RequestId; params: ThreadRealtimeStartParams } - | { - method: "thread/realtime/appendAudio"; - id: RequestId; - params: ThreadRealtimeAppendAudioParams; - } - | { method: "thread/realtime/appendText"; id: RequestId; params: ThreadRealtimeAppendTextParams } - | { - method: "thread/realtime/appendSpeech"; - id: RequestId; - params: ThreadRealtimeAppendSpeechParams; - } - | { method: "thread/realtime/stop"; id: RequestId; params: ThreadRealtimeStopParams } - | { method: "thread/timeline/list"; id: RequestId; params: ThreadTimelineListParams } - | { method: "thread/realtime/listVoices"; id: RequestId; params: ThreadRealtimeListVoicesParams } - | { method: "review/start"; id: RequestId; params: ReviewStartParams } - | { method: "model/list"; id: RequestId; params: ModelListParams } - | { - method: "modelProvider/capabilities/read"; - id: RequestId; - params: ModelProviderCapabilitiesReadParams; - } - | { method: "experimentalFeature/list"; id: RequestId; params: ExperimentalFeatureListParams } - | { method: "permissionProfile/list"; id: RequestId; params: PermissionProfileListParams } - | { - method: "experimentalFeature/enablement/set"; - id: RequestId; - params: ExperimentalFeatureEnablementSetParams; - } - | { method: "remoteControl/enable"; id: RequestId; params: RemoteControlEnableParams | null } - | { method: "remoteControl/disable"; id: RequestId; params: RemoteControlDisableParams | null } - | { method: "remoteControl/status/read"; id: RequestId; params: undefined } - | { - method: "remoteControl/pairing/start"; - id: RequestId; - params: RemoteControlPairingStartParams; - } - | { - method: "remoteControl/pairing/status"; - id: RequestId; - params: RemoteControlPairingStatusParams; - } - | { method: "remoteControl/client/list"; id: RequestId; params: RemoteControlClientsListParams } - | { - method: "remoteControl/client/revoke"; - id: RequestId; - params: RemoteControlClientsRevokeParams; - } - | { method: "collaborationMode/list"; id: RequestId; params: CollaborationModeListParams } - | { method: "mock/experimentalMethod"; id: RequestId; params: MockExperimentalMethodParams } - | { method: "environment/add"; id: RequestId; params: EnvironmentAddParams } - | { method: "environment/info"; id: RequestId; params: EnvironmentInfoParams } - | { method: "environment/status"; id: RequestId; params: EnvironmentStatusParams } - | { method: "mcpServer/oauth/login"; id: RequestId; params: McpServerOauthLoginParams } - | { method: "config/mcpServer/reload"; id: RequestId; params: undefined } - | { method: "mcpServerStatus/list"; id: RequestId; params: ListMcpServerStatusParams } - | { method: "mcpServer/resource/read"; id: RequestId; params: McpResourceReadParams } - | { - method: "mcpServer/event/stream/start"; - id: RequestId; - params: McpServerEventStreamStartParams; - } - | { method: "mcpServer/event/stream/stop"; id: RequestId; params: McpServerEventStreamStopParams } - | { method: "mcpServer/tool/call"; id: RequestId; params: McpServerToolCallParams } - | { method: "windowsSandbox/setupStart"; id: RequestId; params: WindowsSandboxSetupStartParams } - | { method: "windowsSandbox/readiness"; id: RequestId; params: undefined } - | { method: "account/login/start"; id: RequestId; params: LoginAccountParams } - | { method: "account/bedrock/discover"; id: RequestId; params: BedrockDiscoverParams } - | { method: "account/bedrock/setup"; id: RequestId; params: BedrockSetupParams } - | { method: "account/login/cancel"; id: RequestId; params: CancelLoginAccountParams } - | { method: "account/logout"; id: RequestId; params: undefined } - | { method: "account/rateLimits/read"; id: RequestId; params: undefined } - | { - method: "account/rateLimitResetCredit/consume"; - id: RequestId; - params: ConsumeAccountRateLimitResetCreditParams; - } - | { method: "account/usage/read"; id: RequestId; params?: GetAccountTokenUsageParams | undefined } - | { method: "account/workspaceMessages/read"; id: RequestId; params: undefined } - | { - method: "account/sendAddCreditsNudgeEmail"; - id: RequestId; - params: SendAddCreditsNudgeEmailParams; - } - | { method: "feedback/upload"; id: RequestId; params: FeedbackUploadParams } - | { method: "command/exec"; id: RequestId; params: CommandExecParams } - | { method: "command/exec/write"; id: RequestId; params: CommandExecWriteParams } - | { method: "command/exec/terminate"; id: RequestId; params: CommandExecTerminateParams } - | { method: "command/exec/resize"; id: RequestId; params: CommandExecResizeParams } - | { method: "process/spawn"; id: RequestId; params: ProcessSpawnParams } - | { method: "process/writeStdin"; id: RequestId; params: ProcessWriteStdinParams } - | { method: "process/kill"; id: RequestId; params: ProcessKillParams } - | { method: "process/resizePty"; id: RequestId; params: ProcessResizePtyParams } - | { method: "config/read"; id: RequestId; params: ConfigReadParams } - | { method: "externalAgentConfig/detect"; id: RequestId; params: ExternalAgentConfigDetectParams } - | { method: "externalAgentConfig/import"; id: RequestId; params: ExternalAgentConfigImportParams } - | { - method: "externalAgentConfig/import/recordHistory"; - id: RequestId; - params: ExternalAgentConfigImportHistoryRecordParams; - } - | { method: "externalAgentConfig/import/readHistories"; id: RequestId; params: undefined } - | { method: "config/value/write"; id: RequestId; params: ConfigValueWriteParams } - | { method: "config/batchWrite"; id: RequestId; params: ConfigBatchWriteParams } - | { method: "configRequirements/read"; id: RequestId; params: undefined } - | { method: "account/read"; id: RequestId; params: GetAccountParams } - | { method: "getConversationSummary"; id: RequestId; params: GetConversationSummaryParams } - | { method: "gitDiffToRemote"; id: RequestId; params: GitDiffToRemoteParams } - | { method: "getAuthStatus"; id: RequestId; params: GetAuthStatusParams } - | { method: "fuzzyFileSearch"; id: RequestId; params: FuzzyFileSearchParams } - | { - method: "fuzzyFileSearch/sessionStart"; - id: RequestId; - params: FuzzyFileSearchSessionStartParams; - } - | { - method: "fuzzyFileSearch/sessionUpdate"; - id: RequestId; - params: FuzzyFileSearchSessionUpdateParams; - } - | { - method: "fuzzyFileSearch/sessionStop"; - id: RequestId; - params: FuzzyFileSearchSessionStopParams; - }; diff --git a/src/runtimes/openai/generated/CodexResponseHandoffMode.ts b/src/runtimes/openai/generated/CodexResponseHandoffMode.ts deleted file mode 100644 index 3eb90da..0000000 --- a/src/runtimes/openai/generated/CodexResponseHandoffMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CodexResponseHandoffMode = "thinking" | "commentary" | "bemTags"; diff --git a/src/runtimes/openai/generated/ConversationGitInfo.ts b/src/runtimes/openai/generated/ConversationGitInfo.ts deleted file mode 100644 index 8715645..0000000 --- a/src/runtimes/openai/generated/ConversationGitInfo.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConversationGitInfo = { - sha: string | null; - branch: string | null; - origin_url: string | null; -}; diff --git a/src/runtimes/openai/generated/ConversationSummary.ts b/src/runtimes/openai/generated/ConversationSummary.ts deleted file mode 100644 index 1207397..0000000 --- a/src/runtimes/openai/generated/ConversationSummary.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConversationGitInfo } from "./ConversationGitInfo"; -import type { SessionSource } from "./SessionSource"; -import type { ThreadId } from "./ThreadId"; - -export type ConversationSummary = { - conversationId: ThreadId; - path: string; - preview: string; - timestamp: string | null; - updatedAt: string | null; - modelProvider: string; - cwd: string; - cliVersion: string; - source: SessionSource; - gitInfo: ConversationGitInfo | null; -}; diff --git a/src/runtimes/openai/generated/ConversationTextRole.ts b/src/runtimes/openai/generated/ConversationTextRole.ts deleted file mode 100644 index a4d574b..0000000 --- a/src/runtimes/openai/generated/ConversationTextRole.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConversationTextRole = "user" | "developer" | "assistant"; diff --git a/src/runtimes/openai/generated/ExecCommandApprovalParams.ts b/src/runtimes/openai/generated/ExecCommandApprovalParams.ts deleted file mode 100644 index 3e88f05..0000000 --- a/src/runtimes/openai/generated/ExecCommandApprovalParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ParsedCommand } from "./ParsedCommand"; -import type { ThreadId } from "./ThreadId"; - -export type ExecCommandApprovalParams = { - conversationId: ThreadId; - /** - * Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent] - * and [codex_protocol::protocol::ExecCommandEndEvent]. - */ - callId: string; - /** - * Identifier for this specific approval callback. - */ - approvalId: string | null; - command: Array; - cwd: string; - reason: string | null; - parsedCmd: Array; -}; diff --git a/src/runtimes/openai/generated/ExecCommandApprovalResponse.ts b/src/runtimes/openai/generated/ExecCommandApprovalResponse.ts deleted file mode 100644 index 169dab0..0000000 --- a/src/runtimes/openai/generated/ExecCommandApprovalResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReviewDecision } from "./ReviewDecision"; - -export type ExecCommandApprovalResponse = { decision: ReviewDecision }; diff --git a/src/runtimes/openai/generated/ExecPolicyAmendment.ts b/src/runtimes/openai/generated/ExecPolicyAmendment.ts deleted file mode 100644 index 98e2626..0000000 --- a/src/runtimes/openai/generated/ExecPolicyAmendment.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Proposed execpolicy change to allow commands starting with this prefix. - * - * The `command` tokens form the prefix that would be added as an execpolicy - * `prefix_rule(..., decision="allow")`, letting the agent bypass approval for - * commands that start with this token sequence. - */ -export type ExecPolicyAmendment = Array; diff --git a/src/runtimes/openai/generated/FileChange.ts b/src/runtimes/openai/generated/FileChange.ts deleted file mode 100644 index da6abdd..0000000 --- a/src/runtimes/openai/generated/FileChange.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileChange = - | { type: "add"; content: string } - | { type: "delete"; content: string } - | { type: "update"; unified_diff: string; move_path: string | null }; diff --git a/src/runtimes/openai/generated/ForcedLoginMethod.ts b/src/runtimes/openai/generated/ForcedLoginMethod.ts deleted file mode 100644 index c695908..0000000 --- a/src/runtimes/openai/generated/ForcedLoginMethod.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ForcedLoginMethod = "chatgpt" | "api"; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchMatchType.ts b/src/runtimes/openai/generated/FuzzyFileSearchMatchType.ts deleted file mode 100644 index 60e92f9..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchMatchType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchMatchType = "file" | "directory"; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchParams.ts b/src/runtimes/openai/generated/FuzzyFileSearchParams.ts deleted file mode 100644 index 24e397c..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchParams = { - query: string; - roots: Array; - cancellationToken: string | null; -}; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchResponse.ts b/src/runtimes/openai/generated/FuzzyFileSearchResponse.ts deleted file mode 100644 index 454cae2..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult"; - -export type FuzzyFileSearchResponse = { files: Array }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchResult.ts b/src/runtimes/openai/generated/FuzzyFileSearchResult.ts deleted file mode 100644 index f27ea28..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchResult.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType"; - -/** - * Superset of [`codex_file_search::FileMatch`] - */ -export type FuzzyFileSearchResult = { - root: string; - path: string; - match_type: FuzzyFileSearchMatchType; - file_name: string; - score: number; - indices: Array | null; -}; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionCompletedNotification.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionCompletedNotification.ts deleted file mode 100644 index e8f3e39..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchSessionCompletedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionCompletedNotification = { sessionId: string }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionStartParams.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionStartParams.ts deleted file mode 100644 index f746734..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchSessionStartParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionStartParams = { sessionId: string; roots: Array }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionStartResponse.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionStartResponse.ts deleted file mode 100644 index cfe1399..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchSessionStartResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionStartResponse = Record; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionStopParams.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionStopParams.ts deleted file mode 100644 index 72da3c8..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchSessionStopParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionStopParams = { sessionId: string }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionStopResponse.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionStopResponse.ts deleted file mode 100644 index a3500fb..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchSessionStopResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionStopResponse = Record; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateParams.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateParams.ts deleted file mode 100644 index 53542ea..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionUpdateParams = { sessionId: string; query: string }; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateResponse.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateResponse.ts deleted file mode 100644 index 54b8701..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdateResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionUpdateResponse = Record; diff --git a/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdatedNotification.ts b/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdatedNotification.ts deleted file mode 100644 index d993091..0000000 --- a/src/runtimes/openai/generated/FuzzyFileSearchSessionUpdatedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult"; - -export type FuzzyFileSearchSessionUpdatedNotification = { - sessionId: string; - query: string; - files: Array; -}; diff --git a/src/runtimes/openai/generated/GetAuthStatusParams.ts b/src/runtimes/openai/generated/GetAuthStatusParams.ts deleted file mode 100644 index c3f4c1a..0000000 --- a/src/runtimes/openai/generated/GetAuthStatusParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GetAuthStatusParams = { includeToken: boolean | null; refreshToken: boolean | null }; diff --git a/src/runtimes/openai/generated/GetAuthStatusResponse.ts b/src/runtimes/openai/generated/GetAuthStatusResponse.ts deleted file mode 100644 index bad0238..0000000 --- a/src/runtimes/openai/generated/GetAuthStatusResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AuthMode } from "./AuthMode"; - -export type GetAuthStatusResponse = { - authMethod: AuthMode | null; - authToken: string | null; - requiresOpenaiAuth: boolean | null; -}; diff --git a/src/runtimes/openai/generated/GetConversationSummaryParams.ts b/src/runtimes/openai/generated/GetConversationSummaryParams.ts deleted file mode 100644 index b4d1772..0000000 --- a/src/runtimes/openai/generated/GetConversationSummaryParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadId } from "./ThreadId"; - -export type GetConversationSummaryParams = { rolloutPath: string } | { conversationId: ThreadId }; diff --git a/src/runtimes/openai/generated/GetConversationSummaryResponse.ts b/src/runtimes/openai/generated/GetConversationSummaryResponse.ts deleted file mode 100644 index a4848ce..0000000 --- a/src/runtimes/openai/generated/GetConversationSummaryResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConversationSummary } from "./ConversationSummary"; - -export type GetConversationSummaryResponse = { summary: ConversationSummary }; diff --git a/src/runtimes/openai/generated/GitDiffToRemoteParams.ts b/src/runtimes/openai/generated/GitDiffToRemoteParams.ts deleted file mode 100644 index 31ff4f9..0000000 --- a/src/runtimes/openai/generated/GitDiffToRemoteParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GitDiffToRemoteParams = { cwd: string }; diff --git a/src/runtimes/openai/generated/GitDiffToRemoteResponse.ts b/src/runtimes/openai/generated/GitDiffToRemoteResponse.ts deleted file mode 100644 index b3d4d7f..0000000 --- a/src/runtimes/openai/generated/GitDiffToRemoteResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GitSha } from "./GitSha"; - -export type GitDiffToRemoteResponse = { sha: GitSha; diff: string }; diff --git a/src/runtimes/openai/generated/GitSha.ts b/src/runtimes/openai/generated/GitSha.ts deleted file mode 100644 index 701b75a..0000000 --- a/src/runtimes/openai/generated/GitSha.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GitSha = string; diff --git a/src/runtimes/openai/generated/ImageGenerationFailure.ts b/src/runtimes/openai/generated/ImageGenerationFailure.ts deleted file mode 100644 index 19e00d0..0000000 --- a/src/runtimes/openai/generated/ImageGenerationFailure.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ImageGenerationFailure = { - type: "usageLimitExceeded"; - limitId: string; - resetsAt: number | null; -}; diff --git a/src/runtimes/openai/generated/ImageGenerationItem.ts b/src/runtimes/openai/generated/ImageGenerationItem.ts deleted file mode 100644 index 26b36c5..0000000 --- a/src/runtimes/openai/generated/ImageGenerationItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "./AbsolutePathBuf"; -import type { ImageGenerationFailure } from "./ImageGenerationFailure"; - -export type ImageGenerationItem = { - id: string; - status: string; - revisedPrompt: string | null; - result: string; - transparentBackground?: boolean; - failure: ImageGenerationFailure | null; - savedPath?: AbsolutePathBuf; -}; diff --git a/src/runtimes/openai/generated/InitializeResponse.ts b/src/runtimes/openai/generated/InitializeResponse.ts deleted file mode 100644 index 4f4f4f7..0000000 --- a/src/runtimes/openai/generated/InitializeResponse.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "./AbsolutePathBuf"; - -export type InitializeResponse = { - userAgent: string; - /** - * Absolute path to the server's $CODEX_HOME directory. - */ - codexHome: AbsolutePathBuf; - /** - * Platform family for the running app-server target, for example - * `"unix"` or `"windows"`. - */ - platformFamily: string; - /** - * Operating system for the running app-server target, for example - * `"macos"`, `"linux"`, or `"windows"`. - */ - platformOs: string; -}; diff --git a/src/runtimes/openai/generated/InputModality.ts b/src/runtimes/openai/generated/InputModality.ts deleted file mode 100644 index 40d598d..0000000 --- a/src/runtimes/openai/generated/InputModality.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Canonical user-input modality tags advertised by a model. - */ -export type InputModality = "text" | "image" | "audio"; diff --git a/src/runtimes/openai/generated/InternalSessionSource.ts b/src/runtimes/openai/generated/InternalSessionSource.ts deleted file mode 100644 index d847e34..0000000 --- a/src/runtimes/openai/generated/InternalSessionSource.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type InternalSessionSource = "memory_consolidation" | "guardian"; diff --git a/src/runtimes/openai/generated/McpServerInfo.ts b/src/runtimes/openai/generated/McpServerInfo.ts deleted file mode 100644 index d41876f..0000000 --- a/src/runtimes/openai/generated/McpServerInfo.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue"; - -/** - * Presentation metadata advertised by an initialized MCP server. - */ -export type McpServerInfo = { - name: string; - title: string | null; - version: string; - description: string | null; - icons: Array | null; - websiteUrl: string | null; -}; diff --git a/src/runtimes/openai/generated/NetworkPolicyAmendment.ts b/src/runtimes/openai/generated/NetworkPolicyAmendment.ts deleted file mode 100644 index 51348de..0000000 --- a/src/runtimes/openai/generated/NetworkPolicyAmendment.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; - -export type NetworkPolicyAmendment = { host: string; action: NetworkPolicyRuleAction }; diff --git a/src/runtimes/openai/generated/NetworkPolicyRuleAction.ts b/src/runtimes/openai/generated/NetworkPolicyRuleAction.ts deleted file mode 100644 index 55ec700..0000000 --- a/src/runtimes/openai/generated/NetworkPolicyRuleAction.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkPolicyRuleAction = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/ParsedCommand.ts b/src/runtimes/openai/generated/ParsedCommand.ts deleted file mode 100644 index 2302ea1..0000000 --- a/src/runtimes/openai/generated/ParsedCommand.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ParsedCommand = - | { - type: "read"; - cmd: string; - name: string; - /** - * (Best effort) Path to the file being read by the command. When - * possible, this is an absolute path, though when relative, it should - * be resolved against the `cwd`` that will be used to run the command - * to derive the absolute path. - */ - path: string; - } - | { type: "list_files"; cmd: string; path: string | null } - | { type: "search"; cmd: string; query: string | null; path: string | null } - | { type: "unknown"; cmd: string }; diff --git a/src/runtimes/openai/generated/PathUri.ts b/src/runtimes/openai/generated/PathUri.ts deleted file mode 100644 index ba11296..0000000 --- a/src/runtimes/openai/generated/PathUri.ts +++ /dev/null @@ -1,32 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * An immutable, cross-platform representation of a `file:` URI. - * - * Only the `file:` scheme is currently accepted. Construction validates the - * URL, and the URI cannot be mutated after construction. [`Self::basename`], - * [`Self::parent`], and [`Self::join`] operate on URI path segments without - * interpreting them using the operating system running Codex. Fallback URIs - * created by [`Self::from_abs_path`] are opaque to these lexical operations. - * - * `file:` paths retain their URI spelling so they can be parsed independently - * of the current host, except that Windows drive letters are canonicalized to - * uppercase. A local POSIX `file:` URI can also retain percent-encoded non-UTF-8 - * bytes for lossless native round trips. - * - * Like [VS Code resources], path operations use `/` URI separators on every - * host. Lexical path operations preserve a URL authority without interpreting - * Windows drive or UNC roots from path text. Windows path equality and hashing - * ignore ASCII case, while POSIX paths remain case-sensitive. Native path - * normalization, filesystem aliases, symlinks, and Unicode normalization are - * not resolved. - * - * Serde represents a `PathUri` as its canonical URI string. Deserialization - * accepts only valid `file:` URI strings. These strings round-trip through - * their canonical URL form, including encoded non-UTF-8 path bytes. - * - * [VS Code resources]: https://github.com/microsoft/vscode/blob/main/src/vs/base/common/resources.ts - */ -export type PathUri = string; diff --git a/src/runtimes/openai/generated/PlanType.ts b/src/runtimes/openai/generated/PlanType.ts deleted file mode 100644 index a755b86..0000000 --- a/src/runtimes/openai/generated/PlanType.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PlanType = - | "free" - | "go" - | "plus" - | "pro" - | "prolite" - | "team" - | "self_serve_business_prolite" - | "self_serve_business_usage_based" - | "business" - | "ent26" - | "enterprise_cbp_automation" - | "enterprise_cbp_usage_based" - | "enterprise" - | "edu" - | "edu_plus" - | "edu_pro" - | "unknown"; diff --git a/src/runtimes/openai/generated/ProtocolMethods.ts b/src/runtimes/openai/generated/ProtocolMethods.ts new file mode 100644 index 0000000..f4a2116 --- /dev/null +++ b/src/runtimes/openai/generated/ProtocolMethods.ts @@ -0,0 +1,96 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// Derived from the matching runtime JSON Schemas by scripts/prune-openai-generated-types.mjs. +export type ServerNotificationMethod = + | "error" + | "thread/started" + | "thread/status/changed" + | "thread/archived" + | "thread/deleted" + | "thread/unarchived" + | "thread/closed" + | "thread/reverted" + | "skills/changed" + | "thread/name/updated" + | "thread/goal/updated" + | "thread/goal/cleared" + | "thread/queue/changed" + | "project/changed" + | "thread/project/updated" + | "thread/environment/connected" + | "thread/environment/disconnected" + | "thread/settings/updated" + | "thread/tokenUsage/updated" + | "turn/started" + | "hook/started" + | "turn/completed" + | "hook/completed" + | "turn/diff/updated" + | "turn/plan/updated" + | "item/started" + | "item/autoApprovalReview/started" + | "item/autoApprovalReview/completed" + | "autoApprovalReview/strictReviewRequired" + | "item/completed" + | "item/agentMessage/delta" + | "item/plan/delta" + | "command/exec/outputDelta" + | "process/outputDelta" + | "process/exited" + | "item/commandExecution/outputDelta" + | "item/commandExecution/terminalInteraction" + | "item/fileChange/outputDelta" + | "item/fileChange/patchUpdated" + | "serverRequest/resolved" + | "item/mcpToolCall/progress" + | "mcpServer/oauthLogin/completed" + | "mcpServer/startupStatus/updated" + | "mcpServer/event/stream/notification" + | "account/updated" + | "account/rateLimits/updated" + | "app/list/updated" + | "remoteControl/status/changed" + | "externalAgentConfig/import/progress" + | "externalAgentConfig/import/completed" + | "fs/changed" + | "item/reasoning/summaryTextDelta" + | "item/reasoning/summaryPartAdded" + | "item/reasoning/textDelta" + | "thread/compacted" + | "model/rerouted" + | "model/verification" + | "turn/moderationMetadata" + | "model/safetyBuffering/updated" + | "warning" + | "guardianWarning" + | "deprecationNotice" + | "configWarning" + | "fuzzyFileSearch/sessionUpdated" + | "fuzzyFileSearch/sessionCompleted" + | "thread/realtime/started" + | "thread/realtime/itemAdded" + | "thread/realtime/item/started" + | "thread/realtime/item/transcript/delta" + | "thread/realtime/item/completed" + | "thread/realtime/transcript/delta" + | "thread/realtime/transcript/done" + | "thread/realtime/outputAudio/delta" + | "thread/realtime/sdp" + | "thread/realtime/error" + | "thread/realtime/closed" + | "windows/worldWritableWarning" + | "windowsSandbox/setupCompleted" + | "account/login/completed"; + +export type ServerRequestMethod = + | "item/commandExecution/requestApproval" + | "item/fileChange/requestApproval" + | "item/tool/requestUserInput" + | "mcpServer/elicitation/request" + | "item/permissions/requestApproval" + | "item/tool/call" + | "account/chatgptAuthTokens/refresh" + | "attestation/generate" + | "currentTime/read" + | "applyPatchApproval" + | "execCommandApproval"; diff --git a/src/runtimes/openai/generated/README.md b/src/runtimes/openai/generated/README.md index 1cc851b..1904cb2 100644 --- a/src/runtimes/openai/generated/README.md +++ b/src/runtimes/openai/generated/README.md @@ -1,21 +1,25 @@ -# OpenAI app-server protocol +# OpenAI app-server protocol types -The TypeScript files in this directory are generated as one atomic schema set. +This directory contains the transitive closure of the OpenAI app-server types imported by the driver. They correspond to OpenAI app-server runtime version `0.150.1`, which is also pinned by the CLI dependency and container image. -Runtime validation lives in the adjacent `app-server-protocol-*` modules and is typed against this schema. +Runtime validation and the complete supported server method sets come from the adjacent JSON Schemas. -Regenerate them with the matching runtime: +Regenerate those schemas first, then regenerate and prune the TypeScript output with the matching runtime: ```sh +set -eu generated_dir=src/runtimes/openai/generated generated_tmp=$(mktemp -d) trap 'rm -rf "$generated_tmp"' EXIT vp exec codex app-server generate-ts --experimental --out "$generated_tmp" fd --extension ts . "$generated_dir" --exec rm cp -R "$generated_tmp"/. "$generated_dir"/ +bun scripts/prune-openai-generated-types.mjs vp fmt "$generated_dir" --write ``` -Do not split or edit individual generated TypeScript files by hand. +The pruning script follows every direct generated-type import from `app-server-protocol-types.ts`, so upstream fields and their complete dependency closure are retained automatically. + +Do not edit individual generated TypeScript files by hand. diff --git a/src/runtimes/openai/generated/RealtimeConversationVersion.ts b/src/runtimes/openai/generated/RealtimeConversationVersion.ts deleted file mode 100644 index 81b8d31..0000000 --- a/src/runtimes/openai/generated/RealtimeConversationVersion.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RealtimeConversationVersion = "v1" | "v2" | "v3"; diff --git a/src/runtimes/openai/generated/RealtimeOutputModality.ts b/src/runtimes/openai/generated/RealtimeOutputModality.ts deleted file mode 100644 index 78e00e7..0000000 --- a/src/runtimes/openai/generated/RealtimeOutputModality.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RealtimeOutputModality = "text" | "audio"; diff --git a/src/runtimes/openai/generated/RealtimeVoice.ts b/src/runtimes/openai/generated/RealtimeVoice.ts deleted file mode 100644 index bd40db7..0000000 --- a/src/runtimes/openai/generated/RealtimeVoice.ts +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RealtimeVoice = - | "alloy" - | "arbor" - | "ash" - | "ballad" - | "breeze" - | "cedar" - | "coral" - | "cove" - | "echo" - | "ember" - | "juniper" - | "maple" - | "marin" - | "sage" - | "shimmer" - | "sol" - | "spruce" - | "vale" - | "verse"; diff --git a/src/runtimes/openai/generated/RealtimeVoicesList.ts b/src/runtimes/openai/generated/RealtimeVoicesList.ts deleted file mode 100644 index d18fcdf..0000000 --- a/src/runtimes/openai/generated/RealtimeVoicesList.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RealtimeVoice } from "./RealtimeVoice"; - -export type RealtimeVoicesList = { - v1: Array; - v2: Array; - defaultV1: RealtimeVoice; - defaultV2: RealtimeVoice; -}; diff --git a/src/runtimes/openai/generated/Resource.ts b/src/runtimes/openai/generated/Resource.ts deleted file mode 100644 index 680a8eb..0000000 --- a/src/runtimes/openai/generated/Resource.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue"; - -/** - * A known resource that the server is capable of reading. - */ -export type Resource = { - annotations?: JsonValue; - description?: string; - mimeType?: string; - name: string; - size?: number; - title?: string; - uri: string; - icons?: Array; - _meta?: JsonValue; -}; diff --git a/src/runtimes/openai/generated/ResourceContent.ts b/src/runtimes/openai/generated/ResourceContent.ts deleted file mode 100644 index d40d995..0000000 --- a/src/runtimes/openai/generated/ResourceContent.ts +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue"; - -/** - * Contents returned when reading a resource from an MCP server. - */ -export type ResourceContent = - | { - /** - * The URI of this resource. - */ - uri: string; - mimeType?: string; - text: string; - _meta?: JsonValue; - } - | { - /** - * The URI of this resource. - */ - uri: string; - mimeType?: string; - blob: string; - _meta?: JsonValue; - }; diff --git a/src/runtimes/openai/generated/ResourceTemplate.ts b/src/runtimes/openai/generated/ResourceTemplate.ts deleted file mode 100644 index 6c31477..0000000 --- a/src/runtimes/openai/generated/ResourceTemplate.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue"; - -/** - * A template description for resources available on the server. - */ -export type ResourceTemplate = { - annotations?: JsonValue; - uriTemplate: string; - name: string; - title?: string; - description?: string; - mimeType?: string; -}; diff --git a/src/runtimes/openai/generated/ReviewDecision.ts b/src/runtimes/openai/generated/ReviewDecision.ts deleted file mode 100644 index 8251c74..0000000 --- a/src/runtimes/openai/generated/ReviewDecision.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; -import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; - -/** - * User's decision in response to an ExecApprovalRequest. - */ -export type ReviewDecision = - | "approved" - | { approved_execpolicy_amendment: { proposed_execpolicy_amendment: ExecPolicyAmendment } } - | "approved_for_session" - | "approved_mcp_policy_amendment" - | { network_policy_amendment: { network_policy_amendment: NetworkPolicyAmendment } } - | { denied: { rejection: string } } - | "timed_out" - | "abort"; diff --git a/src/runtimes/openai/generated/ServerNotification.ts b/src/runtimes/openai/generated/ServerNotification.ts deleted file mode 100644 index f285532..0000000 --- a/src/runtimes/openai/generated/ServerNotification.ts +++ /dev/null @@ -1,193 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification"; -import type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification"; -import type { AccountLoginCompletedNotification } from "./v2/AccountLoginCompletedNotification"; -import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimitsUpdatedNotification"; -import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification"; -import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification"; -import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification"; -import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification"; -import type { CommandExecutionOutputDeltaNotification } from "./v2/CommandExecutionOutputDeltaNotification"; -import type { ConfigWarningNotification } from "./v2/ConfigWarningNotification"; -import type { ContextCompactedNotification } from "./v2/ContextCompactedNotification"; -import type { DeprecationNoticeNotification } from "./v2/DeprecationNoticeNotification"; -import type { EnvironmentConnectionNotification } from "./v2/EnvironmentConnectionNotification"; -import type { ErrorNotification } from "./v2/ErrorNotification"; -import type { ExternalAgentConfigImportCompletedNotification } from "./v2/ExternalAgentConfigImportCompletedNotification"; -import type { ExternalAgentConfigImportProgressNotification } from "./v2/ExternalAgentConfigImportProgressNotification"; -import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDeltaNotification"; -import type { FileChangePatchUpdatedNotification } from "./v2/FileChangePatchUpdatedNotification"; -import type { FsChangedNotification } from "./v2/FsChangedNotification"; -import type { GuardianWarningNotification } from "./v2/GuardianWarningNotification"; -import type { HookCompletedNotification } from "./v2/HookCompletedNotification"; -import type { HookStartedNotification } from "./v2/HookStartedNotification"; -import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification"; -import type { ItemGuardianApprovalReviewCompletedNotification } from "./v2/ItemGuardianApprovalReviewCompletedNotification"; -import type { ItemGuardianApprovalReviewStartedNotification } from "./v2/ItemGuardianApprovalReviewStartedNotification"; -import type { ItemStartedNotification } from "./v2/ItemStartedNotification"; -import type { McpServerEventStreamNotification } from "./v2/McpServerEventStreamNotification"; -import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification"; -import type { McpServerStatusUpdatedNotification } from "./v2/McpServerStatusUpdatedNotification"; -import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification"; -import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification"; -import type { ModelSafetyBufferingUpdatedNotification } from "./v2/ModelSafetyBufferingUpdatedNotification"; -import type { ModelVerificationNotification } from "./v2/ModelVerificationNotification"; -import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification"; -import type { ProcessExitedNotification } from "./v2/ProcessExitedNotification"; -import type { ProcessOutputDeltaNotification } from "./v2/ProcessOutputDeltaNotification"; -import type { ProjectChangedNotification } from "./v2/ProjectChangedNotification"; -import type { RawResponseCompletedNotification } from "./v2/RawResponseCompletedNotification"; -import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification"; -import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification"; -import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification"; -import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification"; -import type { RemoteControlStatusChangedNotification } from "./v2/RemoteControlStatusChangedNotification"; -import type { ServerRequestResolvedNotification } from "./v2/ServerRequestResolvedNotification"; -import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification"; -import type { StrictReviewRequiredNotification } from "./v2/StrictReviewRequiredNotification"; -import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; -import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification"; -import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification"; -import type { ThreadDeletedNotification } from "./v2/ThreadDeletedNotification"; -import type { ThreadGoalClearedNotification } from "./v2/ThreadGoalClearedNotification"; -import type { ThreadGoalUpdatedNotification } from "./v2/ThreadGoalUpdatedNotification"; -import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification"; -import type { ThreadProjectUpdatedNotification } from "./v2/ThreadProjectUpdatedNotification"; -import type { ThreadQueueChangedNotification } from "./v2/ThreadQueueChangedNotification"; -import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosedNotification"; -import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification"; -import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification"; -import type { ThreadRealtimeItemCompletedNotification } from "./v2/ThreadRealtimeItemCompletedNotification"; -import type { ThreadRealtimeItemStartedNotification } from "./v2/ThreadRealtimeItemStartedNotification"; -import type { ThreadRealtimeItemTranscriptDeltaNotification } from "./v2/ThreadRealtimeItemTranscriptDeltaNotification"; -import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification"; -import type { ThreadRealtimeSdpNotification } from "./v2/ThreadRealtimeSdpNotification"; -import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification"; -import type { ThreadRealtimeTranscriptDeltaNotification } from "./v2/ThreadRealtimeTranscriptDeltaNotification"; -import type { ThreadRealtimeTranscriptDoneNotification } from "./v2/ThreadRealtimeTranscriptDoneNotification"; -import type { ThreadRevertedNotification } from "./v2/ThreadRevertedNotification"; -import type { ThreadSettingsUpdatedNotification } from "./v2/ThreadSettingsUpdatedNotification"; -import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification"; -import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification"; -import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification"; -import type { ThreadUnarchivedNotification } from "./v2/ThreadUnarchivedNotification"; -import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification"; -import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification"; -import type { TurnModerationMetadataNotification } from "./v2/TurnModerationMetadataNotification"; -import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification"; -import type { TurnStartedNotification } from "./v2/TurnStartedNotification"; -import type { WarningNotification } from "./v2/WarningNotification"; -import type { WindowsSandboxSetupCompletedNotification } from "./v2/WindowsSandboxSetupCompletedNotification"; -import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldWritableWarningNotification"; - -/** - * Notification sent from the server to the client. - */ -export type ServerNotification = - | { method: "error"; params: ErrorNotification } - | { method: "thread/started"; params: ThreadStartedNotification } - | { method: "thread/status/changed"; params: ThreadStatusChangedNotification } - | { method: "thread/archived"; params: ThreadArchivedNotification } - | { method: "thread/deleted"; params: ThreadDeletedNotification } - | { method: "thread/unarchived"; params: ThreadUnarchivedNotification } - | { method: "thread/closed"; params: ThreadClosedNotification } - | { method: "thread/reverted"; params: ThreadRevertedNotification } - | { method: "skills/changed"; params: SkillsChangedNotification } - | { method: "thread/name/updated"; params: ThreadNameUpdatedNotification } - | { method: "thread/goal/updated"; params: ThreadGoalUpdatedNotification } - | { method: "thread/goal/cleared"; params: ThreadGoalClearedNotification } - | { method: "thread/queue/changed"; params: ThreadQueueChangedNotification } - | { method: "project/changed"; params: ProjectChangedNotification } - | { method: "thread/project/updated"; params: ThreadProjectUpdatedNotification } - | { method: "thread/environment/connected"; params: EnvironmentConnectionNotification } - | { method: "thread/environment/disconnected"; params: EnvironmentConnectionNotification } - | { method: "thread/settings/updated"; params: ThreadSettingsUpdatedNotification } - | { method: "thread/tokenUsage/updated"; params: ThreadTokenUsageUpdatedNotification } - | { method: "turn/started"; params: TurnStartedNotification } - | { method: "hook/started"; params: HookStartedNotification } - | { method: "turn/completed"; params: TurnCompletedNotification } - | { method: "hook/completed"; params: HookCompletedNotification } - | { method: "turn/diff/updated"; params: TurnDiffUpdatedNotification } - | { method: "turn/plan/updated"; params: TurnPlanUpdatedNotification } - | { method: "item/started"; params: ItemStartedNotification } - | { - method: "item/autoApprovalReview/started"; - params: ItemGuardianApprovalReviewStartedNotification; - } - | { - method: "item/autoApprovalReview/completed"; - params: ItemGuardianApprovalReviewCompletedNotification; - } - | { method: "autoApprovalReview/strictReviewRequired"; params: StrictReviewRequiredNotification } - | { method: "item/completed"; params: ItemCompletedNotification } - | { method: "rawResponseItem/completed"; params: RawResponseItemCompletedNotification } - | { method: "rawResponse/completed"; params: RawResponseCompletedNotification } - | { method: "item/agentMessage/delta"; params: AgentMessageDeltaNotification } - | { method: "item/plan/delta"; params: PlanDeltaNotification } - | { method: "command/exec/outputDelta"; params: CommandExecOutputDeltaNotification } - | { method: "process/outputDelta"; params: ProcessOutputDeltaNotification } - | { method: "process/exited"; params: ProcessExitedNotification } - | { method: "item/commandExecution/outputDelta"; params: CommandExecutionOutputDeltaNotification } - | { method: "item/commandExecution/terminalInteraction"; params: TerminalInteractionNotification } - | { method: "item/fileChange/outputDelta"; params: FileChangeOutputDeltaNotification } - | { method: "item/fileChange/patchUpdated"; params: FileChangePatchUpdatedNotification } - | { method: "serverRequest/resolved"; params: ServerRequestResolvedNotification } - | { method: "item/mcpToolCall/progress"; params: McpToolCallProgressNotification } - | { method: "mcpServer/oauthLogin/completed"; params: McpServerOauthLoginCompletedNotification } - | { method: "mcpServer/startupStatus/updated"; params: McpServerStatusUpdatedNotification } - | { method: "mcpServer/event/stream/notification"; params: McpServerEventStreamNotification } - | { method: "account/updated"; params: AccountUpdatedNotification } - | { method: "account/rateLimits/updated"; params: AccountRateLimitsUpdatedNotification } - | { method: "app/list/updated"; params: AppListUpdatedNotification } - | { method: "remoteControl/status/changed"; params: RemoteControlStatusChangedNotification } - | { - method: "externalAgentConfig/import/progress"; - params: ExternalAgentConfigImportProgressNotification; - } - | { - method: "externalAgentConfig/import/completed"; - params: ExternalAgentConfigImportCompletedNotification; - } - | { method: "fs/changed"; params: FsChangedNotification } - | { method: "item/reasoning/summaryTextDelta"; params: ReasoningSummaryTextDeltaNotification } - | { method: "item/reasoning/summaryPartAdded"; params: ReasoningSummaryPartAddedNotification } - | { method: "item/reasoning/textDelta"; params: ReasoningTextDeltaNotification } - | { method: "thread/compacted"; params: ContextCompactedNotification } - | { method: "model/rerouted"; params: ModelReroutedNotification } - | { method: "model/verification"; params: ModelVerificationNotification } - | { method: "turn/moderationMetadata"; params: TurnModerationMetadataNotification } - | { method: "model/safetyBuffering/updated"; params: ModelSafetyBufferingUpdatedNotification } - | { method: "warning"; params: WarningNotification } - | { method: "guardianWarning"; params: GuardianWarningNotification } - | { method: "deprecationNotice"; params: DeprecationNoticeNotification } - | { method: "configWarning"; params: ConfigWarningNotification } - | { method: "fuzzyFileSearch/sessionUpdated"; params: FuzzyFileSearchSessionUpdatedNotification } - | { - method: "fuzzyFileSearch/sessionCompleted"; - params: FuzzyFileSearchSessionCompletedNotification; - } - | { method: "thread/realtime/started"; params: ThreadRealtimeStartedNotification } - | { method: "thread/realtime/itemAdded"; params: ThreadRealtimeItemAddedNotification } - | { method: "thread/realtime/item/started"; params: ThreadRealtimeItemStartedNotification } - | { - method: "thread/realtime/item/transcript/delta"; - params: ThreadRealtimeItemTranscriptDeltaNotification; - } - | { method: "thread/realtime/item/completed"; params: ThreadRealtimeItemCompletedNotification } - | { - method: "thread/realtime/transcript/delta"; - params: ThreadRealtimeTranscriptDeltaNotification; - } - | { method: "thread/realtime/transcript/done"; params: ThreadRealtimeTranscriptDoneNotification } - | { - method: "thread/realtime/outputAudio/delta"; - params: ThreadRealtimeOutputAudioDeltaNotification; - } - | { method: "thread/realtime/sdp"; params: ThreadRealtimeSdpNotification } - | { method: "thread/realtime/error"; params: ThreadRealtimeErrorNotification } - | { method: "thread/realtime/closed"; params: ThreadRealtimeClosedNotification } - | { method: "windows/worldWritableWarning"; params: WindowsWorldWritableWarningNotification } - | { method: "windowsSandbox/setupCompleted"; params: WindowsSandboxSetupCompletedNotification } - | { method: "account/login/completed"; params: AccountLoginCompletedNotification }; diff --git a/src/runtimes/openai/generated/ServerNotificationEnvelope.ts b/src/runtimes/openai/generated/ServerNotificationEnvelope.ts deleted file mode 100644 index ba54079..0000000 --- a/src/runtimes/openai/generated/ServerNotificationEnvelope.ts +++ /dev/null @@ -1,205 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification"; -import type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification"; -import type { AccountLoginCompletedNotification } from "./v2/AccountLoginCompletedNotification"; -import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimitsUpdatedNotification"; -import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification"; -import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification"; -import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification"; -import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification"; -import type { CommandExecutionOutputDeltaNotification } from "./v2/CommandExecutionOutputDeltaNotification"; -import type { ConfigWarningNotification } from "./v2/ConfigWarningNotification"; -import type { ContextCompactedNotification } from "./v2/ContextCompactedNotification"; -import type { DeprecationNoticeNotification } from "./v2/DeprecationNoticeNotification"; -import type { EnvironmentConnectionNotification } from "./v2/EnvironmentConnectionNotification"; -import type { ErrorNotification } from "./v2/ErrorNotification"; -import type { ExternalAgentConfigImportCompletedNotification } from "./v2/ExternalAgentConfigImportCompletedNotification"; -import type { ExternalAgentConfigImportProgressNotification } from "./v2/ExternalAgentConfigImportProgressNotification"; -import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDeltaNotification"; -import type { FileChangePatchUpdatedNotification } from "./v2/FileChangePatchUpdatedNotification"; -import type { FsChangedNotification } from "./v2/FsChangedNotification"; -import type { GuardianWarningNotification } from "./v2/GuardianWarningNotification"; -import type { HookCompletedNotification } from "./v2/HookCompletedNotification"; -import type { HookStartedNotification } from "./v2/HookStartedNotification"; -import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification"; -import type { ItemGuardianApprovalReviewCompletedNotification } from "./v2/ItemGuardianApprovalReviewCompletedNotification"; -import type { ItemGuardianApprovalReviewStartedNotification } from "./v2/ItemGuardianApprovalReviewStartedNotification"; -import type { ItemStartedNotification } from "./v2/ItemStartedNotification"; -import type { McpServerEventStreamNotification } from "./v2/McpServerEventStreamNotification"; -import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification"; -import type { McpServerStatusUpdatedNotification } from "./v2/McpServerStatusUpdatedNotification"; -import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification"; -import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification"; -import type { ModelSafetyBufferingUpdatedNotification } from "./v2/ModelSafetyBufferingUpdatedNotification"; -import type { ModelVerificationNotification } from "./v2/ModelVerificationNotification"; -import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification"; -import type { ProcessExitedNotification } from "./v2/ProcessExitedNotification"; -import type { ProcessOutputDeltaNotification } from "./v2/ProcessOutputDeltaNotification"; -import type { ProjectChangedNotification } from "./v2/ProjectChangedNotification"; -import type { RawResponseCompletedNotification } from "./v2/RawResponseCompletedNotification"; -import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification"; -import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification"; -import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification"; -import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification"; -import type { RemoteControlStatusChangedNotification } from "./v2/RemoteControlStatusChangedNotification"; -import type { ServerRequestResolvedNotification } from "./v2/ServerRequestResolvedNotification"; -import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification"; -import type { StrictReviewRequiredNotification } from "./v2/StrictReviewRequiredNotification"; -import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; -import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification"; -import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification"; -import type { ThreadDeletedNotification } from "./v2/ThreadDeletedNotification"; -import type { ThreadGoalClearedNotification } from "./v2/ThreadGoalClearedNotification"; -import type { ThreadGoalUpdatedNotification } from "./v2/ThreadGoalUpdatedNotification"; -import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification"; -import type { ThreadProjectUpdatedNotification } from "./v2/ThreadProjectUpdatedNotification"; -import type { ThreadQueueChangedNotification } from "./v2/ThreadQueueChangedNotification"; -import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosedNotification"; -import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification"; -import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification"; -import type { ThreadRealtimeItemCompletedNotification } from "./v2/ThreadRealtimeItemCompletedNotification"; -import type { ThreadRealtimeItemStartedNotification } from "./v2/ThreadRealtimeItemStartedNotification"; -import type { ThreadRealtimeItemTranscriptDeltaNotification } from "./v2/ThreadRealtimeItemTranscriptDeltaNotification"; -import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification"; -import type { ThreadRealtimeSdpNotification } from "./v2/ThreadRealtimeSdpNotification"; -import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification"; -import type { ThreadRealtimeTranscriptDeltaNotification } from "./v2/ThreadRealtimeTranscriptDeltaNotification"; -import type { ThreadRealtimeTranscriptDoneNotification } from "./v2/ThreadRealtimeTranscriptDoneNotification"; -import type { ThreadRevertedNotification } from "./v2/ThreadRevertedNotification"; -import type { ThreadSettingsUpdatedNotification } from "./v2/ThreadSettingsUpdatedNotification"; -import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification"; -import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification"; -import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification"; -import type { ThreadUnarchivedNotification } from "./v2/ThreadUnarchivedNotification"; -import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification"; -import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification"; -import type { TurnModerationMetadataNotification } from "./v2/TurnModerationMetadataNotification"; -import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification"; -import type { TurnStartedNotification } from "./v2/TurnStartedNotification"; -import type { WarningNotification } from "./v2/WarningNotification"; -import type { WindowsSandboxSetupCompletedNotification } from "./v2/WindowsSandboxSetupCompletedNotification"; -import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldWritableWarningNotification"; - -/** - * Server notification envelope sent over app-server transports. - * - * `emitted_at_ms` records when app-server emitted the notification, before it - * is fanned out to individual connections. - */ -export type ServerNotificationEnvelope = { - /** - * Unix timestamp (in milliseconds) when app-server emitted this notification. - * - * Optional so clients can decode notifications from older app-server - * versions. Current app-server versions always populate it. - */ - emittedAtMs?: number; -} & ( - | { method: "error"; params: ErrorNotification } - | { method: "thread/started"; params: ThreadStartedNotification } - | { method: "thread/status/changed"; params: ThreadStatusChangedNotification } - | { method: "thread/archived"; params: ThreadArchivedNotification } - | { method: "thread/deleted"; params: ThreadDeletedNotification } - | { method: "thread/unarchived"; params: ThreadUnarchivedNotification } - | { method: "thread/closed"; params: ThreadClosedNotification } - | { method: "thread/reverted"; params: ThreadRevertedNotification } - | { method: "skills/changed"; params: SkillsChangedNotification } - | { method: "thread/name/updated"; params: ThreadNameUpdatedNotification } - | { method: "thread/goal/updated"; params: ThreadGoalUpdatedNotification } - | { method: "thread/goal/cleared"; params: ThreadGoalClearedNotification } - | { method: "thread/queue/changed"; params: ThreadQueueChangedNotification } - | { method: "project/changed"; params: ProjectChangedNotification } - | { method: "thread/project/updated"; params: ThreadProjectUpdatedNotification } - | { method: "thread/environment/connected"; params: EnvironmentConnectionNotification } - | { method: "thread/environment/disconnected"; params: EnvironmentConnectionNotification } - | { method: "thread/settings/updated"; params: ThreadSettingsUpdatedNotification } - | { method: "thread/tokenUsage/updated"; params: ThreadTokenUsageUpdatedNotification } - | { method: "turn/started"; params: TurnStartedNotification } - | { method: "hook/started"; params: HookStartedNotification } - | { method: "turn/completed"; params: TurnCompletedNotification } - | { method: "hook/completed"; params: HookCompletedNotification } - | { method: "turn/diff/updated"; params: TurnDiffUpdatedNotification } - | { method: "turn/plan/updated"; params: TurnPlanUpdatedNotification } - | { method: "item/started"; params: ItemStartedNotification } - | { - method: "item/autoApprovalReview/started"; - params: ItemGuardianApprovalReviewStartedNotification; - } - | { - method: "item/autoApprovalReview/completed"; - params: ItemGuardianApprovalReviewCompletedNotification; - } - | { method: "autoApprovalReview/strictReviewRequired"; params: StrictReviewRequiredNotification } - | { method: "item/completed"; params: ItemCompletedNotification } - | { method: "rawResponseItem/completed"; params: RawResponseItemCompletedNotification } - | { method: "rawResponse/completed"; params: RawResponseCompletedNotification } - | { method: "item/agentMessage/delta"; params: AgentMessageDeltaNotification } - | { method: "item/plan/delta"; params: PlanDeltaNotification } - | { method: "command/exec/outputDelta"; params: CommandExecOutputDeltaNotification } - | { method: "process/outputDelta"; params: ProcessOutputDeltaNotification } - | { method: "process/exited"; params: ProcessExitedNotification } - | { method: "item/commandExecution/outputDelta"; params: CommandExecutionOutputDeltaNotification } - | { method: "item/commandExecution/terminalInteraction"; params: TerminalInteractionNotification } - | { method: "item/fileChange/outputDelta"; params: FileChangeOutputDeltaNotification } - | { method: "item/fileChange/patchUpdated"; params: FileChangePatchUpdatedNotification } - | { method: "serverRequest/resolved"; params: ServerRequestResolvedNotification } - | { method: "item/mcpToolCall/progress"; params: McpToolCallProgressNotification } - | { method: "mcpServer/oauthLogin/completed"; params: McpServerOauthLoginCompletedNotification } - | { method: "mcpServer/startupStatus/updated"; params: McpServerStatusUpdatedNotification } - | { method: "mcpServer/event/stream/notification"; params: McpServerEventStreamNotification } - | { method: "account/updated"; params: AccountUpdatedNotification } - | { method: "account/rateLimits/updated"; params: AccountRateLimitsUpdatedNotification } - | { method: "app/list/updated"; params: AppListUpdatedNotification } - | { method: "remoteControl/status/changed"; params: RemoteControlStatusChangedNotification } - | { - method: "externalAgentConfig/import/progress"; - params: ExternalAgentConfigImportProgressNotification; - } - | { - method: "externalAgentConfig/import/completed"; - params: ExternalAgentConfigImportCompletedNotification; - } - | { method: "fs/changed"; params: FsChangedNotification } - | { method: "item/reasoning/summaryTextDelta"; params: ReasoningSummaryTextDeltaNotification } - | { method: "item/reasoning/summaryPartAdded"; params: ReasoningSummaryPartAddedNotification } - | { method: "item/reasoning/textDelta"; params: ReasoningTextDeltaNotification } - | { method: "thread/compacted"; params: ContextCompactedNotification } - | { method: "model/rerouted"; params: ModelReroutedNotification } - | { method: "model/verification"; params: ModelVerificationNotification } - | { method: "turn/moderationMetadata"; params: TurnModerationMetadataNotification } - | { method: "model/safetyBuffering/updated"; params: ModelSafetyBufferingUpdatedNotification } - | { method: "warning"; params: WarningNotification } - | { method: "guardianWarning"; params: GuardianWarningNotification } - | { method: "deprecationNotice"; params: DeprecationNoticeNotification } - | { method: "configWarning"; params: ConfigWarningNotification } - | { method: "fuzzyFileSearch/sessionUpdated"; params: FuzzyFileSearchSessionUpdatedNotification } - | { - method: "fuzzyFileSearch/sessionCompleted"; - params: FuzzyFileSearchSessionCompletedNotification; - } - | { method: "thread/realtime/started"; params: ThreadRealtimeStartedNotification } - | { method: "thread/realtime/itemAdded"; params: ThreadRealtimeItemAddedNotification } - | { method: "thread/realtime/item/started"; params: ThreadRealtimeItemStartedNotification } - | { - method: "thread/realtime/item/transcript/delta"; - params: ThreadRealtimeItemTranscriptDeltaNotification; - } - | { method: "thread/realtime/item/completed"; params: ThreadRealtimeItemCompletedNotification } - | { - method: "thread/realtime/transcript/delta"; - params: ThreadRealtimeTranscriptDeltaNotification; - } - | { method: "thread/realtime/transcript/done"; params: ThreadRealtimeTranscriptDoneNotification } - | { - method: "thread/realtime/outputAudio/delta"; - params: ThreadRealtimeOutputAudioDeltaNotification; - } - | { method: "thread/realtime/sdp"; params: ThreadRealtimeSdpNotification } - | { method: "thread/realtime/error"; params: ThreadRealtimeErrorNotification } - | { method: "thread/realtime/closed"; params: ThreadRealtimeClosedNotification } - | { method: "windows/worldWritableWarning"; params: WindowsWorldWritableWarningNotification } - | { method: "windowsSandbox/setupCompleted"; params: WindowsSandboxSetupCompletedNotification } - | { method: "account/login/completed"; params: AccountLoginCompletedNotification } -); diff --git a/src/runtimes/openai/generated/ServerRequest.ts b/src/runtimes/openai/generated/ServerRequest.ts deleted file mode 100644 index f2b11e7..0000000 --- a/src/runtimes/openai/generated/ServerRequest.ts +++ /dev/null @@ -1,51 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams"; -import type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams"; -import type { RequestId } from "./RequestId"; -import type { AttestationGenerateParams } from "./v2/AttestationGenerateParams"; -import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefreshParams"; -import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams"; -import type { CurrentTimeReadParams } from "./v2/CurrentTimeReadParams"; -import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams"; -import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams"; -import type { McpServerElicitationRequestParams } from "./v2/McpServerElicitationRequestParams"; -import type { PermissionsRequestApprovalParams } from "./v2/PermissionsRequestApprovalParams"; -import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams"; - -/** - * Request initiated from the server and sent to the client. - */ -export type ServerRequest = - | { - method: "item/commandExecution/requestApproval"; - id: RequestId; - params: CommandExecutionRequestApprovalParams; - } - | { - method: "item/fileChange/requestApproval"; - id: RequestId; - params: FileChangeRequestApprovalParams; - } - | { method: "item/tool/requestUserInput"; id: RequestId; params: ToolRequestUserInputParams } - | { - method: "mcpServer/elicitation/request"; - id: RequestId; - params: McpServerElicitationRequestParams; - } - | { - method: "item/permissions/requestApproval"; - id: RequestId; - params: PermissionsRequestApprovalParams; - } - | { method: "item/tool/call"; id: RequestId; params: DynamicToolCallParams } - | { - method: "account/chatgptAuthTokens/refresh"; - id: RequestId; - params: ChatgptAuthTokensRefreshParams; - } - | { method: "attestation/generate"; id: RequestId; params: AttestationGenerateParams } - | { method: "currentTime/read"; id: RequestId; params: CurrentTimeReadParams } - | { method: "applyPatchApproval"; id: RequestId; params: ApplyPatchApprovalParams } - | { method: "execCommandApproval"; id: RequestId; params: ExecCommandApprovalParams }; diff --git a/src/runtimes/openai/generated/SessionSource.ts b/src/runtimes/openai/generated/SessionSource.ts deleted file mode 100644 index 592597e..0000000 --- a/src/runtimes/openai/generated/SessionSource.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { InternalSessionSource } from "./InternalSessionSource"; -import type { SubAgentSource } from "./SubAgentSource"; - -export type SessionSource = - | "cli" - | "vscode" - | "exec" - | "mcp" - | { custom: string } - | { internal: InternalSessionSource } - | { subagent: SubAgentSource } - | "unknown"; diff --git a/src/runtimes/openai/generated/SleepItem.ts b/src/runtimes/openai/generated/SleepItem.ts deleted file mode 100644 index 8d9ddaa..0000000 --- a/src/runtimes/openai/generated/SleepItem.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Display item emitted by the interruptible `clock.sleep` tool. - */ -export type SleepItem = { id: string; durationMs: number }; diff --git a/src/runtimes/openai/generated/SubAgentSource.ts b/src/runtimes/openai/generated/SubAgentSource.ts deleted file mode 100644 index ea3a7a7..0000000 --- a/src/runtimes/openai/generated/SubAgentSource.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AgentPath } from "./AgentPath"; -import type { ThreadId } from "./ThreadId"; - -export type SubAgentSource = - | "review" - | "compact" - | { - thread_spawn: { - parent_thread_id: ThreadId; - depth: number; - agent_path: AgentPath | null; - agent_nickname: string | null; - agent_role: string | null; - }; - } - | "memory_consolidation" - | { other: string }; diff --git a/src/runtimes/openai/generated/ThreadId.ts b/src/runtimes/openai/generated/ThreadId.ts deleted file mode 100644 index 801ffb3..0000000 --- a/src/runtimes/openai/generated/ThreadId.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Identifier for a Codex thread. - * - * Codex-generated thread IDs are UUIDv7, and some use cases rely on that. - */ -export type ThreadId = string; diff --git a/src/runtimes/openai/generated/ThreadMemoryMode.ts b/src/runtimes/openai/generated/ThreadMemoryMode.ts deleted file mode 100644 index 74a7e75..0000000 --- a/src/runtimes/openai/generated/ThreadMemoryMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadMemoryMode = "enabled" | "disabled"; diff --git a/src/runtimes/openai/generated/Tool.ts b/src/runtimes/openai/generated/Tool.ts deleted file mode 100644 index aed8b7a..0000000 --- a/src/runtimes/openai/generated/Tool.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue"; - -/** - * Definition for a tool the client can call. - */ -export type Tool = { - name: string; - title?: string; - description?: string; - inputSchema: JsonValue; - outputSchema?: JsonValue; - annotations?: JsonValue; - icons?: Array; - _meta?: JsonValue; -}; diff --git a/src/runtimes/openai/generated/Verbosity.ts b/src/runtimes/openai/generated/Verbosity.ts deleted file mode 100644 index 8fd97b0..0000000 --- a/src/runtimes/openai/generated/Verbosity.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Controls output length/detail on GPT-5 models via the Responses API. - * Serialized with lowercase values to match the OpenAI API. - */ -export type Verbosity = "low" | "medium" | "high"; diff --git a/src/runtimes/openai/generated/WebSearchContextSize.ts b/src/runtimes/openai/generated/WebSearchContextSize.ts deleted file mode 100644 index d6feedd..0000000 --- a/src/runtimes/openai/generated/WebSearchContextSize.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSearchContextSize = "low" | "medium" | "high"; diff --git a/src/runtimes/openai/generated/WebSearchItem.ts b/src/runtimes/openai/generated/WebSearchItem.ts deleted file mode 100644 index 73932cb..0000000 --- a/src/runtimes/openai/generated/WebSearchItem.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue"; -import type { WebSearchAction } from "./v2/WebSearchAction"; - -export type WebSearchItem = { - id: string; - query: string; - action: WebSearchAction | null; - /** - * Structured search results returned out-of-band by standalone web search. - * - * These stay as opaque JSON at the extension/app-server boundary so new - * result fields and result types can pass through without a Codex release. - */ - results: Array | null; -}; diff --git a/src/runtimes/openai/generated/WebSearchLocation.ts b/src/runtimes/openai/generated/WebSearchLocation.ts deleted file mode 100644 index 4f257ee..0000000 --- a/src/runtimes/openai/generated/WebSearchLocation.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSearchLocation = { - country: string | null; - region: string | null; - city: string | null; - timezone: string | null; -}; diff --git a/src/runtimes/openai/generated/WebSearchMode.ts b/src/runtimes/openai/generated/WebSearchMode.ts deleted file mode 100644 index 0544fd0..0000000 --- a/src/runtimes/openai/generated/WebSearchMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSearchMode = "disabled" | "cached" | "indexed" | "live"; diff --git a/src/runtimes/openai/generated/WebSearchToolConfig.ts b/src/runtimes/openai/generated/WebSearchToolConfig.ts deleted file mode 100644 index 518f6d8..0000000 --- a/src/runtimes/openai/generated/WebSearchToolConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WebSearchContextSize } from "./WebSearchContextSize"; -import type { WebSearchLocation } from "./WebSearchLocation"; - -export type WebSearchToolConfig = { - context_size: WebSearchContextSize | null; - allowed_domains: Array | null; - location: WebSearchLocation | null; -}; diff --git a/src/runtimes/openai/generated/index.ts b/src/runtimes/openai/generated/index.ts deleted file mode 100644 index cb39e54..0000000 --- a/src/runtimes/openai/generated/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -export type { AbsolutePathBuf } from "./AbsolutePathBuf"; -export type { AgentMessageInputContent } from "./AgentMessageInputContent"; -export type { AgentPath } from "./AgentPath"; -export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams"; -export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse"; -export type { AuthMode } from "./AuthMode"; -export type { AutoCompactTokenLimitScope } from "./AutoCompactTokenLimitScope"; -export type { ClientInfo } from "./ClientInfo"; -export type { ClientNotification } from "./ClientNotification"; -export type { ClientRequest } from "./ClientRequest"; -export type { CodexResponseHandoffMode } from "./CodexResponseHandoffMode"; -export type { CollaborationMode } from "./CollaborationMode"; -export type { ContentItem } from "./ContentItem"; -export type { ConversationGitInfo } from "./ConversationGitInfo"; -export type { ConversationSummary } from "./ConversationSummary"; -export type { ConversationTextRole } from "./ConversationTextRole"; -export type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams"; -export type { ExecCommandApprovalResponse } from "./ExecCommandApprovalResponse"; -export type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; -export type { FileChange } from "./FileChange"; -export type { ForcedLoginMethod } from "./ForcedLoginMethod"; -export type { FunctionCallOutputBody } from "./FunctionCallOutputBody"; -export type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem"; -export type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType"; -export type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams"; -export type { FuzzyFileSearchResponse } from "./FuzzyFileSearchResponse"; -export type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult"; -export type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification"; -export type { FuzzyFileSearchSessionStartParams } from "./FuzzyFileSearchSessionStartParams"; -export type { FuzzyFileSearchSessionStartResponse } from "./FuzzyFileSearchSessionStartResponse"; -export type { FuzzyFileSearchSessionStopParams } from "./FuzzyFileSearchSessionStopParams"; -export type { FuzzyFileSearchSessionStopResponse } from "./FuzzyFileSearchSessionStopResponse"; -export type { FuzzyFileSearchSessionUpdateParams } from "./FuzzyFileSearchSessionUpdateParams"; -export type { FuzzyFileSearchSessionUpdateResponse } from "./FuzzyFileSearchSessionUpdateResponse"; -export type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification"; -export type { GetAuthStatusParams } from "./GetAuthStatusParams"; -export type { GetAuthStatusResponse } from "./GetAuthStatusResponse"; -export type { GetConversationSummaryParams } from "./GetConversationSummaryParams"; -export type { GetConversationSummaryResponse } from "./GetConversationSummaryResponse"; -export type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams"; -export type { GitDiffToRemoteResponse } from "./GitDiffToRemoteResponse"; -export type { GitSha } from "./GitSha"; -export type { ImageDetail } from "./ImageDetail"; -export type { ImageGenerationFailure } from "./ImageGenerationFailure"; -export type { ImageGenerationItem } from "./ImageGenerationItem"; -export type { InitializeCapabilities } from "./InitializeCapabilities"; -export type { InitializeParams } from "./InitializeParams"; -export type { InitializeResponse } from "./InitializeResponse"; -export type { InputModality } from "./InputModality"; -export type { InternalChatMessageMetadataPassthrough } from "./InternalChatMessageMetadataPassthrough"; -export type { InternalSessionSource } from "./InternalSessionSource"; -export type { LegacyAppPathString } from "./LegacyAppPathString"; -export type { LocalShellAction } from "./LocalShellAction"; -export type { LocalShellExecAction } from "./LocalShellExecAction"; -export type { LocalShellStatus } from "./LocalShellStatus"; -export type { McpServerInfo } from "./McpServerInfo"; -export type { MessagePhase } from "./MessagePhase"; -export type { ModeKind } from "./ModeKind"; -export type { MultiAgentMode } from "./MultiAgentMode"; -export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; -export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; -export type { ParsedCommand } from "./ParsedCommand"; -export type { PathUri } from "./PathUri"; -export type { Personality } from "./Personality"; -export type { PlanType } from "./PlanType"; -export type { RealtimeConversationVersion } from "./RealtimeConversationVersion"; -export type { RealtimeOutputModality } from "./RealtimeOutputModality"; -export type { RealtimeVoice } from "./RealtimeVoice"; -export type { RealtimeVoicesList } from "./RealtimeVoicesList"; -export type { ReasoningEffort } from "./ReasoningEffort"; -export type { ReasoningItemContent } from "./ReasoningItemContent"; -export type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary"; -export type { ReasoningSummary } from "./ReasoningSummary"; -export type { RequestId } from "./RequestId"; -export type { Resource } from "./Resource"; -export type { ResourceContent } from "./ResourceContent"; -export type { ResourceTemplate } from "./ResourceTemplate"; -export type { ResponseItem } from "./ResponseItem"; -export type { ResponseItemId } from "./ResponseItemId"; -export type { ReviewDecision } from "./ReviewDecision"; -export type { ServerNotification } from "./ServerNotification"; -export type { ServerNotificationEnvelope } from "./ServerNotificationEnvelope"; -export type { ServerRequest } from "./ServerRequest"; -export type { SessionSource } from "./SessionSource"; -export type { Settings } from "./Settings"; -export type { SleepItem } from "./SleepItem"; -export type { SubAgentSource } from "./SubAgentSource"; -export type { ThreadId } from "./ThreadId"; -export type { ThreadMemoryMode } from "./ThreadMemoryMode"; -export type { Tool } from "./Tool"; -export type { Verbosity } from "./Verbosity"; -export type { WebSearchAction } from "./WebSearchAction"; -export type { WebSearchContextSize } from "./WebSearchContextSize"; -export type { WebSearchItem } from "./WebSearchItem"; -export type { WebSearchLocation } from "./WebSearchLocation"; -export type { WebSearchMode } from "./WebSearchMode"; -export type { WebSearchToolConfig } from "./WebSearchToolConfig"; -export * as v2 from "./v2"; diff --git a/src/runtimes/openai/generated/v2/Account.ts b/src/runtimes/openai/generated/v2/Account.ts deleted file mode 100644 index fa7efca..0000000 --- a/src/runtimes/openai/generated/v2/Account.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PlanType } from "../PlanType"; - -export type Account = - | { type: "apiKey" } - | { type: "chatgpt"; email: string | null; planType: PlanType } - | { type: "amazonBedrock"; usesCodexManagedCredentials: boolean }; diff --git a/src/runtimes/openai/generated/v2/AccountLoginCompletedNotification.ts b/src/runtimes/openai/generated/v2/AccountLoginCompletedNotification.ts deleted file mode 100644 index b5ad74e..0000000 --- a/src/runtimes/openai/generated/v2/AccountLoginCompletedNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DesktopOnboardingEntrypoint } from "./DesktopOnboardingEntrypoint"; - -export type AccountLoginCompletedNotification = { - loginId: string | null; - success: boolean; - error: string | null; - onboardingEntrypoint: DesktopOnboardingEntrypoint | null; -}; diff --git a/src/runtimes/openai/generated/v2/AccountRateLimitsUpdatedNotification.ts b/src/runtimes/openai/generated/v2/AccountRateLimitsUpdatedNotification.ts deleted file mode 100644 index 163b608..0000000 --- a/src/runtimes/openai/generated/v2/AccountRateLimitsUpdatedNotification.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RateLimitSnapshot } from "./RateLimitSnapshot"; - -/** - * Sparse rolling rate-limit update. - * - * Clients should merge available values into the most recent `account/rateLimits/read` response - * or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and - * does not clear a previously observed value. - */ -export type AccountRateLimitsUpdatedNotification = { rateLimits: RateLimitSnapshot }; diff --git a/src/runtimes/openai/generated/v2/AccountTokenUsageDailyBucket.ts b/src/runtimes/openai/generated/v2/AccountTokenUsageDailyBucket.ts deleted file mode 100644 index a9c3f9a..0000000 --- a/src/runtimes/openai/generated/v2/AccountTokenUsageDailyBucket.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AccountTokenUsageDailyBucket = { startDate: string; tokens: bigint }; diff --git a/src/runtimes/openai/generated/v2/AccountTokenUsageSummary.ts b/src/runtimes/openai/generated/v2/AccountTokenUsageSummary.ts deleted file mode 100644 index 23adb9e..0000000 --- a/src/runtimes/openai/generated/v2/AccountTokenUsageSummary.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AccountTokenUsageSummary = { - lifetimeTokens: bigint | null; - peakDailyTokens: bigint | null; - longestRunningTurnSec: bigint | null; - currentStreakDays: bigint | null; - longestStreakDays: bigint | null; -}; diff --git a/src/runtimes/openai/generated/v2/AccountUpdatedNotification.ts b/src/runtimes/openai/generated/v2/AccountUpdatedNotification.ts deleted file mode 100644 index e1862ac..0000000 --- a/src/runtimes/openai/generated/v2/AccountUpdatedNotification.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AuthMode } from "../AuthMode"; -import type { PlanType } from "../PlanType"; - -export type AccountUpdatedNotification = { authMode: AuthMode | null; planType: PlanType | null }; diff --git a/src/runtimes/openai/generated/v2/ActivePermissionProfile.ts b/src/runtimes/openai/generated/v2/ActivePermissionProfile.ts deleted file mode 100644 index 48f258a..0000000 --- a/src/runtimes/openai/generated/v2/ActivePermissionProfile.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ActivePermissionProfile = { - /** - * Identifier from `default_permissions` or the implicit built-in default, - * such as `:workspace` or a user-defined `[permissions.]` profile. - */ - id: string; - /** - * Parent profile identifier from the selected permissions profile's - * `extends` setting, when present. - */ - extends: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/AddCreditsNudgeCreditType.ts b/src/runtimes/openai/generated/v2/AddCreditsNudgeCreditType.ts deleted file mode 100644 index 70498d6..0000000 --- a/src/runtimes/openai/generated/v2/AddCreditsNudgeCreditType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AddCreditsNudgeCreditType = "credits" | "usage_limit"; diff --git a/src/runtimes/openai/generated/v2/AddCreditsNudgeEmailStatus.ts b/src/runtimes/openai/generated/v2/AddCreditsNudgeEmailStatus.ts deleted file mode 100644 index 2b62da6..0000000 --- a/src/runtimes/openai/generated/v2/AddCreditsNudgeEmailStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AddCreditsNudgeEmailStatus = "sent" | "cooldown_active"; diff --git a/src/runtimes/openai/generated/v2/AdditionalFileSystemPermissions.ts b/src/runtimes/openai/generated/v2/AdditionalFileSystemPermissions.ts deleted file mode 100644 index 6b09707..0000000 --- a/src/runtimes/openai/generated/v2/AdditionalFileSystemPermissions.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LegacyAppPathString } from "../LegacyAppPathString"; -import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry"; - -export type AdditionalFileSystemPermissions = { - /** - * This will be removed in favor of `entries`. - */ - read: Array | null; - /** - * This will be removed in favor of `entries`. - */ - write: Array | null; - globScanMaxDepth?: number; - entries?: Array; -}; diff --git a/src/runtimes/openai/generated/v2/AdditionalNetworkPermissions.ts b/src/runtimes/openai/generated/v2/AdditionalNetworkPermissions.ts deleted file mode 100644 index 9349aa6..0000000 --- a/src/runtimes/openai/generated/v2/AdditionalNetworkPermissions.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AdditionalNetworkPermissions = { enabled: boolean | null }; diff --git a/src/runtimes/openai/generated/v2/AdditionalPermissionProfile.ts b/src/runtimes/openai/generated/v2/AdditionalPermissionProfile.ts deleted file mode 100644 index 108472d..0000000 --- a/src/runtimes/openai/generated/v2/AdditionalPermissionProfile.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; -import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; - -export type AdditionalPermissionProfile = { - /** - * Partial overlay used for per-command permission requests. - */ - network: AdditionalNetworkPermissions | null; - fileSystem: AdditionalFileSystemPermissions | null; -}; diff --git a/src/runtimes/openai/generated/v2/AgentMessageDelivery.ts b/src/runtimes/openai/generated/v2/AgentMessageDelivery.ts deleted file mode 100644 index bc6547e..0000000 --- a/src/runtimes/openai/generated/v2/AgentMessageDelivery.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AgentMessageDelivery = "async"; diff --git a/src/runtimes/openai/generated/v2/AgentMessageDeltaNotification.ts b/src/runtimes/openai/generated/v2/AgentMessageDeltaNotification.ts deleted file mode 100644 index 1beb2e9..0000000 --- a/src/runtimes/openai/generated/v2/AgentMessageDeltaNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AgentMessageDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; -}; diff --git a/src/runtimes/openai/generated/v2/AllowDenyRequirement.ts b/src/runtimes/openai/generated/v2/AllowDenyRequirement.ts deleted file mode 100644 index ec13796..0000000 --- a/src/runtimes/openai/generated/v2/AllowDenyRequirement.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AllowDenyRequirement = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/v2/AnalyticsConfig.ts b/src/runtimes/openai/generated/v2/AnalyticsConfig.ts deleted file mode 100644 index 4e36555..0000000 --- a/src/runtimes/openai/generated/v2/AnalyticsConfig.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type AnalyticsConfig = { enabled: boolean | null } & { - [key in string]?: - | number - | string - | boolean - | Array - | { [key in string]?: JsonValue } - | null; -}; diff --git a/src/runtimes/openai/generated/v2/AppBranding.ts b/src/runtimes/openai/generated/v2/AppBranding.ts deleted file mode 100644 index 8a2999e..0000000 --- a/src/runtimes/openai/generated/v2/AppBranding.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - app metadata returned by app-list APIs. - */ -export type AppBranding = { - category: string | null; - developer: string | null; - website: string | null; - privacyPolicy: string | null; - termsOfService: string | null; - isDiscoverableApp: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/AppInfo.ts b/src/runtimes/openai/generated/v2/AppInfo.ts deleted file mode 100644 index 8838c0a..0000000 --- a/src/runtimes/openai/generated/v2/AppInfo.ts +++ /dev/null @@ -1,34 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppBranding } from "./AppBranding"; -import type { AppMetadata } from "./AppMetadata"; - -/** - * EXPERIMENTAL - app metadata returned by app-list APIs. - */ -export type AppInfo = { - id: string; - name: string; - description: string | null; - logoUrl: string | null; - logoUrlDark: string | null; - iconAssets: { [key in string]?: string } | null; - iconDarkAssets: { [key in string]?: string } | null; - distributionChannel: string | null; - branding: AppBranding | null; - appMetadata: AppMetadata | null; - labels: { [key in string]?: string } | null; - installUrl: string | null; - isAccessible: boolean; - /** - * Whether this app is enabled in config.toml. - * Example: - * ```toml - * [apps.bad_app] - * enabled = false - * ``` - */ - isEnabled: boolean; - pluginDisplayNames: Array; -}; diff --git a/src/runtimes/openai/generated/v2/AppListUpdatedNotification.ts b/src/runtimes/openai/generated/v2/AppListUpdatedNotification.ts deleted file mode 100644 index 794ce09..0000000 --- a/src/runtimes/openai/generated/v2/AppListUpdatedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppInfo } from "./AppInfo"; - -/** - * EXPERIMENTAL - notification emitted when the app list changes. - */ -export type AppListUpdatedNotification = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/AppMetadata.ts b/src/runtimes/openai/generated/v2/AppMetadata.ts deleted file mode 100644 index 1377fd2..0000000 --- a/src/runtimes/openai/generated/v2/AppMetadata.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppReview } from "./AppReview"; -import type { AppScreenshot } from "./AppScreenshot"; - -export type AppMetadata = { - review: AppReview | null; - categories: Array | null; - subCategories: Array | null; - seoDescription: string | null; - screenshots: Array | null; - developer: string | null; - version: string | null; - versionId: string | null; - versionNotes: string | null; - firstPartyRequiresInstall: boolean | null; - showInComposerWhenUnlinked: boolean | null; -}; diff --git a/src/runtimes/openai/generated/v2/AppReview.ts b/src/runtimes/openai/generated/v2/AppReview.ts deleted file mode 100644 index 96e14b7..0000000 --- a/src/runtimes/openai/generated/v2/AppReview.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AppReview = { status: string }; diff --git a/src/runtimes/openai/generated/v2/AppScreenshot.ts b/src/runtimes/openai/generated/v2/AppScreenshot.ts deleted file mode 100644 index f538ccc..0000000 --- a/src/runtimes/openai/generated/v2/AppScreenshot.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AppScreenshot = { url: string | null; fileId: string | null; userPrompt: string }; diff --git a/src/runtimes/openai/generated/v2/AppSummary.ts b/src/runtimes/openai/generated/v2/AppSummary.ts deleted file mode 100644 index 7fddf8c..0000000 --- a/src/runtimes/openai/generated/v2/AppSummary.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - app metadata summary for plugin responses. - */ -export type AppSummary = { - id: string; - name: string; - description: string | null; - installUrl: string | null; - category: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/AppTemplateSummary.ts b/src/runtimes/openai/generated/v2/AppTemplateSummary.ts deleted file mode 100644 index cabea63..0000000 --- a/src/runtimes/openai/generated/v2/AppTemplateSummary.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppTemplateUnavailableReason } from "./AppTemplateUnavailableReason"; - -export type AppTemplateSummary = { - templateId: string; - name: string; - description: string | null; - category: string | null; - canonicalConnectorId: string | null; - logoUrl: string | null; - logoUrlDark: string | null; - materializedAppIds: Array; - reason: AppTemplateUnavailableReason | null; -}; diff --git a/src/runtimes/openai/generated/v2/AppTemplateUnavailableReason.ts b/src/runtimes/openai/generated/v2/AppTemplateUnavailableReason.ts deleted file mode 100644 index 5630567..0000000 --- a/src/runtimes/openai/generated/v2/AppTemplateUnavailableReason.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AppTemplateUnavailableReason = "NOT_CONFIGURED_FOR_WORKSPACE" | "NO_ACTIVE_WORKSPACE"; diff --git a/src/runtimes/openai/generated/v2/AppToolApproval.ts b/src/runtimes/openai/generated/v2/AppToolApproval.ts deleted file mode 100644 index 6704ef0..0000000 --- a/src/runtimes/openai/generated/v2/AppToolApproval.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AppToolApproval = "auto" | "prompt" | "writes" | "approve"; diff --git a/src/runtimes/openai/generated/v2/AppToolSummary.ts b/src/runtimes/openai/generated/v2/AppToolSummary.ts deleted file mode 100644 index 138ad25..0000000 --- a/src/runtimes/openai/generated/v2/AppToolSummary.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - metadata returned by app/read. - */ -export type AppToolSummary = { - name: string; - title: string | null; - description: string; - isEnabled: boolean; - disabledReason: string | null; - isReadOnly: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/AppToolsConfig.ts b/src/runtimes/openai/generated/v2/AppToolsConfig.ts deleted file mode 100644 index f7f3650..0000000 --- a/src/runtimes/openai/generated/v2/AppToolsConfig.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppToolApproval } from "./AppToolApproval"; - -export type AppToolsConfig = { - [key in string]?: { enabled: boolean | null; approval_mode: AppToolApproval | null }; -}; diff --git a/src/runtimes/openai/generated/v2/AppsConfig.ts b/src/runtimes/openai/generated/v2/AppsConfig.ts deleted file mode 100644 index 1fee163..0000000 --- a/src/runtimes/openai/generated/v2/AppsConfig.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppToolApproval } from "./AppToolApproval"; -import type { AppToolsConfig } from "./AppToolsConfig"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AppsDefaultConfig } from "./AppsDefaultConfig"; - -export type AppsConfig = { _default: AppsDefaultConfig | null } & { - [key in string]?: { - enabled: boolean; - approvals_reviewer: ApprovalsReviewer | null; - destructive_enabled: boolean | null; - open_world_enabled: boolean | null; - default_tools_approval_mode: AppToolApproval | null; - default_tools_enabled: boolean | null; - tools: AppToolsConfig | null; - }; -}; diff --git a/src/runtimes/openai/generated/v2/AppsDefaultConfig.ts b/src/runtimes/openai/generated/v2/AppsDefaultConfig.ts deleted file mode 100644 index 9ca967e..0000000 --- a/src/runtimes/openai/generated/v2/AppsDefaultConfig.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppToolApproval } from "./AppToolApproval"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; - -export type AppsDefaultConfig = { - enabled: boolean; - approvals_reviewer: ApprovalsReviewer | null; - destructive_enabled: boolean; - open_world_enabled: boolean; - default_tools_approval_mode: AppToolApproval | null; -}; diff --git a/src/runtimes/openai/generated/v2/AppsInstalledParams.ts b/src/runtimes/openai/generated/v2/AppsInstalledParams.ts deleted file mode 100644 index fab4440..0000000 --- a/src/runtimes/openai/generated/v2/AppsInstalledParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Read the committed installed connector runtime snapshot. - */ -export type AppsInstalledParams = { - /** - * Optional loaded thread id used to evaluate effective app configuration. - */ - threadId?: string | null; - /** - * When true and Apps are permitted, refresh and publish the hosted connector runtime tool - * snapshot first. - */ - forceRefresh?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/AppsInstalledResponse.ts b/src/runtimes/openai/generated/v2/AppsInstalledResponse.ts deleted file mode 100644 index 69af3df..0000000 --- a/src/runtimes/openai/generated/v2/AppsInstalledResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { InstalledApp } from "./InstalledApp"; - -/** - * The installed connectors in one committed runtime snapshot. - */ -export type AppsInstalledResponse = { apps: Array }; diff --git a/src/runtimes/openai/generated/v2/AppsListParams.ts b/src/runtimes/openai/generated/v2/AppsListParams.ts deleted file mode 100644 index e85212c..0000000 --- a/src/runtimes/openai/generated/v2/AppsListParams.ts +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - list available apps/connectors. - */ -export type AppsListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a reasonable server-side value. - */ - limit?: number | null; - /** - * Optional thread id used to evaluate app feature gating from that thread's config. - */ - threadId?: string | null; - /** - * When true, bypass app caches and fetch the latest data from sources. - */ - forceRefetch?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/AppsListResponse.ts b/src/runtimes/openai/generated/v2/AppsListResponse.ts deleted file mode 100644 index 0ac62a4..0000000 --- a/src/runtimes/openai/generated/v2/AppsListResponse.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppInfo } from "./AppInfo"; - -/** - * EXPERIMENTAL - app list response. - */ -export type AppsListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/AppsReadParams.ts b/src/runtimes/openai/generated/v2/AppsReadParams.ts deleted file mode 100644 index 1345cb3..0000000 --- a/src/runtimes/openai/generated/v2/AppsReadParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - read metadata for specific apps/connectors. - */ -export type AppsReadParams = { - /** - * App ids to read. The server accepts at most 100 ids and deduplicates repeated ids while - * preserving their first-request order. - */ - appIds: Array; - /** - * Optional loaded thread id used to evaluate effective app configuration. - */ - threadId?: string | null; - /** - * When true, include display-only public tool summaries in the returned metadata. - */ - includeTools?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/AppsReadResponse.ts b/src/runtimes/openai/generated/v2/AppsReadResponse.ts deleted file mode 100644 index e90e772..0000000 --- a/src/runtimes/openai/generated/v2/AppsReadResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConnectorMetadata } from "./ConnectorMetadata"; - -/** - * EXPERIMENTAL - app/read response. - */ -export type AppsReadResponse = { apps: Array; missingAppIds: Array }; diff --git a/src/runtimes/openai/generated/v2/AttestationGenerateParams.ts b/src/runtimes/openai/generated/v2/AttestationGenerateParams.ts deleted file mode 100644 index 0e87e7d..0000000 --- a/src/runtimes/openai/generated/v2/AttestationGenerateParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AttestationGenerateParams = Record; diff --git a/src/runtimes/openai/generated/v2/AttestationGenerateResponse.ts b/src/runtimes/openai/generated/v2/AttestationGenerateResponse.ts deleted file mode 100644 index 0dd4b0a..0000000 --- a/src/runtimes/openai/generated/v2/AttestationGenerateResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AttestationGenerateResponse = { - /** - * Opaque client attestation token. - */ - token: string; -}; diff --git a/src/runtimes/openai/generated/v2/AutoReviewDecisionSource.ts b/src/runtimes/openai/generated/v2/AutoReviewDecisionSource.ts deleted file mode 100644 index 8806981..0000000 --- a/src/runtimes/openai/generated/v2/AutoReviewDecisionSource.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * [UNSTABLE] Source that produced a terminal approval auto-review decision. - */ -export type AutoReviewDecisionSource = "agent"; diff --git a/src/runtimes/openai/generated/v2/AutoReviewRequirements.ts b/src/runtimes/openai/generated/v2/AutoReviewRequirements.ts deleted file mode 100644 index 57bd31a..0000000 --- a/src/runtimes/openai/generated/v2/AutoReviewRequirements.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AutoReviewRequirements = { - requiredOnModels: Array | null; - ignoreRules: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/AwsCredentialType.ts b/src/runtimes/openai/generated/v2/AwsCredentialType.ts deleted file mode 100644 index cc88efa..0000000 --- a/src/runtimes/openai/generated/v2/AwsCredentialType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AwsCredentialType = "accessKeys" | "bedrockApiKey"; diff --git a/src/runtimes/openai/generated/v2/BedrockAwsProfile.ts b/src/runtimes/openai/generated/v2/BedrockAwsProfile.ts deleted file mode 100644 index 53eac31..0000000 --- a/src/runtimes/openai/generated/v2/BedrockAwsProfile.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type BedrockAwsProfile = { name: string; region: string | null }; diff --git a/src/runtimes/openai/generated/v2/BedrockDiscoverParams.ts b/src/runtimes/openai/generated/v2/BedrockDiscoverParams.ts deleted file mode 100644 index 61ab5fc..0000000 --- a/src/runtimes/openai/generated/v2/BedrockDiscoverParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type BedrockDiscoverParams = Record; diff --git a/src/runtimes/openai/generated/v2/BedrockDiscoverResponse.ts b/src/runtimes/openai/generated/v2/BedrockDiscoverResponse.ts deleted file mode 100644 index 8222e42..0000000 --- a/src/runtimes/openai/generated/v2/BedrockDiscoverResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BedrockAwsProfile } from "./BedrockAwsProfile"; -import type { BedrockEnvironmentCredential } from "./BedrockEnvironmentCredential"; - -export type BedrockDiscoverResponse = { - profiles: Array; - environmentCredentials: Array; -}; diff --git a/src/runtimes/openai/generated/v2/BedrockEnvironmentCredential.ts b/src/runtimes/openai/generated/v2/BedrockEnvironmentCredential.ts deleted file mode 100644 index 52b1347..0000000 --- a/src/runtimes/openai/generated/v2/BedrockEnvironmentCredential.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AwsCredentialType } from "./AwsCredentialType"; - -export type BedrockEnvironmentCredential = { type: AwsCredentialType; region: string | null }; diff --git a/src/runtimes/openai/generated/v2/BedrockSetupParams.ts b/src/runtimes/openai/generated/v2/BedrockSetupParams.ts deleted file mode 100644 index 5325b06..0000000 --- a/src/runtimes/openai/generated/v2/BedrockSetupParams.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type BedrockSetupParams = - | { type: "profile"; profile: string; region: string } - | { type: "environment"; region: string }; diff --git a/src/runtimes/openai/generated/v2/BedrockSetupResponse.ts b/src/runtimes/openai/generated/v2/BedrockSetupResponse.ts deleted file mode 100644 index c76ba9b..0000000 --- a/src/runtimes/openai/generated/v2/BedrockSetupResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type BedrockSetupResponse = Record; diff --git a/src/runtimes/openai/generated/v2/BrowserUseAccessApprovalLifetime.ts b/src/runtimes/openai/generated/v2/BrowserUseAccessApprovalLifetime.ts deleted file mode 100644 index f86b59e..0000000 --- a/src/runtimes/openai/generated/v2/BrowserUseAccessApprovalLifetime.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type BrowserUseAccessApprovalLifetime = "turn" | "thread"; diff --git a/src/runtimes/openai/generated/v2/BrowserUseConfig.ts b/src/runtimes/openai/generated/v2/BrowserUseConfig.ts deleted file mode 100644 index b8f159f..0000000 --- a/src/runtimes/openai/generated/v2/BrowserUseConfig.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BrowserUseOriginPolicyConfig } from "./BrowserUseOriginPolicyConfig"; - -export type BrowserUseConfig = { - allow_history_access: boolean | null; - default_origin_policy: BrowserUseOriginPolicyConfig | null; - origins: { [key in string]?: BrowserUseOriginPolicyConfig } | null; -}; diff --git a/src/runtimes/openai/generated/v2/BrowserUseOriginPolicy.ts b/src/runtimes/openai/generated/v2/BrowserUseOriginPolicy.ts deleted file mode 100644 index 9614c39..0000000 --- a/src/runtimes/openai/generated/v2/BrowserUseOriginPolicy.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; -import type { BrowserUseAccessApprovalLifetime } from "./BrowserUseAccessApprovalLifetime"; - -export type BrowserUseOriginPolicy = { - access: AllowDenyRequirement | null; - downloads: AllowDenyRequirement | null; - uploads: AllowDenyRequirement | null; - fullCdpAccess: AllowDenyRequirement | null; - autoReview: AllowDenyRequirement | null; - persistentApproval: boolean | null; - accessApprovalLifetime: BrowserUseAccessApprovalLifetime | null; -}; diff --git a/src/runtimes/openai/generated/v2/BrowserUseOriginPolicyConfig.ts b/src/runtimes/openai/generated/v2/BrowserUseOriginPolicyConfig.ts deleted file mode 100644 index 5dce8f1..0000000 --- a/src/runtimes/openai/generated/v2/BrowserUseOriginPolicyConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; - -export type BrowserUseOriginPolicyConfig = { - access: AllowDenyRequirement | null; - downloads: AllowDenyRequirement | null; - uploads: AllowDenyRequirement | null; - full_cdp_access: AllowDenyRequirement | null; -}; diff --git a/src/runtimes/openai/generated/v2/BrowserUseRequirements.ts b/src/runtimes/openai/generated/v2/BrowserUseRequirements.ts deleted file mode 100644 index 7fa1bd9..0000000 --- a/src/runtimes/openai/generated/v2/BrowserUseRequirements.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BrowserUseOriginPolicy } from "./BrowserUseOriginPolicy"; - -export type BrowserUseRequirements = { - allowHistoryAccess: boolean | null; - disableAutoReview: boolean | null; - allowGlobalPersistentApproval: boolean | null; - defaultOriginPolicy: BrowserUseOriginPolicy | null; - origins: { [key in string]?: BrowserUseOriginPolicy } | null; -}; diff --git a/src/runtimes/openai/generated/v2/CancelLoginAccountParams.ts b/src/runtimes/openai/generated/v2/CancelLoginAccountParams.ts deleted file mode 100644 index 8096042..0000000 --- a/src/runtimes/openai/generated/v2/CancelLoginAccountParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CancelLoginAccountParams = { loginId: string }; diff --git a/src/runtimes/openai/generated/v2/CancelLoginAccountResponse.ts b/src/runtimes/openai/generated/v2/CancelLoginAccountResponse.ts deleted file mode 100644 index a207c5e..0000000 --- a/src/runtimes/openai/generated/v2/CancelLoginAccountResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CancelLoginAccountStatus } from "./CancelLoginAccountStatus"; - -export type CancelLoginAccountResponse = { status: CancelLoginAccountStatus }; diff --git a/src/runtimes/openai/generated/v2/CancelLoginAccountStatus.ts b/src/runtimes/openai/generated/v2/CancelLoginAccountStatus.ts deleted file mode 100644 index bd851c6..0000000 --- a/src/runtimes/openai/generated/v2/CancelLoginAccountStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CancelLoginAccountStatus = "canceled" | "notFound"; diff --git a/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshParams.ts b/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshParams.ts deleted file mode 100644 index 092838e..0000000 --- a/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ChatgptAuthTokensRefreshReason } from "./ChatgptAuthTokensRefreshReason"; - -export type ChatgptAuthTokensRefreshParams = { - reason: ChatgptAuthTokensRefreshReason; - /** - * Workspace/account identifier that Codex was previously using. - * - * Clients that manage multiple accounts/workspaces can use this as a hint - * to refresh the token for the correct workspace. - * - * This may be `null` when the prior auth state did not include a workspace - * identifier (`chatgpt_account_id`). - */ - previousAccountId?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshReason.ts b/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshReason.ts deleted file mode 100644 index ac4006b..0000000 --- a/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshReason.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ChatgptAuthTokensRefreshReason = "unauthorized"; diff --git a/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshResponse.ts b/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshResponse.ts deleted file mode 100644 index 0605988..0000000 --- a/src/runtimes/openai/generated/v2/ChatgptAuthTokensRefreshResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ChatgptAuthTokensRefreshResponse = { - accessToken: string; - chatgptAccountId: string; - chatgptPlanType: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/CliAuthCredentialsStoreMode.ts b/src/runtimes/openai/generated/v2/CliAuthCredentialsStoreMode.ts deleted file mode 100644 index 366d748..0000000 --- a/src/runtimes/openai/generated/v2/CliAuthCredentialsStoreMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CliAuthCredentialsStoreMode = "file" | "keyring" | "auto" | "ephemeral"; diff --git a/src/runtimes/openai/generated/v2/CodexErrorInfo.ts b/src/runtimes/openai/generated/v2/CodexErrorInfo.ts deleted file mode 100644 index 549d101..0000000 --- a/src/runtimes/openai/generated/v2/CodexErrorInfo.ts +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NonSteerableTurnKind } from "./NonSteerableTurnKind"; - -/** - * This translation layer make sure that we expose codex error code in camel case. - * - * When an upstream HTTP status is available (for example, from the Responses API or a provider), - * it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant. - */ -export type CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | { httpConnectionFailed: { httpStatusCode: number | null } } - | { responseStreamConnectionFailed: { httpStatusCode: number | null } } - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | { responseStreamDisconnected: { httpStatusCode: number | null } } - | { responseTooManyFailedAttempts: { httpStatusCode: number | null } } - | { activeTurnNotSteerable: { turnKind: NonSteerableTurnKind } } - | "other"; diff --git a/src/runtimes/openai/generated/v2/CollabAgentState.ts b/src/runtimes/openai/generated/v2/CollabAgentState.ts deleted file mode 100644 index 0bfff00..0000000 --- a/src/runtimes/openai/generated/v2/CollabAgentState.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CollabAgentStatus } from "./CollabAgentStatus"; - -export type CollabAgentState = { status: CollabAgentStatus; message: string | null }; diff --git a/src/runtimes/openai/generated/v2/CollabAgentStatus.ts b/src/runtimes/openai/generated/v2/CollabAgentStatus.ts deleted file mode 100644 index 5bff8e1..0000000 --- a/src/runtimes/openai/generated/v2/CollabAgentStatus.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CollabAgentStatus = - | "pendingInit" - | "running" - | "interrupted" - | "completed" - | "errored" - | "shutdown" - | "notFound"; diff --git a/src/runtimes/openai/generated/v2/CollabAgentTool.ts b/src/runtimes/openai/generated/v2/CollabAgentTool.ts deleted file mode 100644 index 9ce62ac..0000000 --- a/src/runtimes/openai/generated/v2/CollabAgentTool.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CollabAgentTool = - | "spawnAgent" - | "sendInput" - | "resumeAgent" - | "wait" - | "closeAgent" - | "sendMessage" - | "followupTask" - | "interruptAgent" - | "listAgents"; diff --git a/src/runtimes/openai/generated/v2/CollabAgentToolCallStatus.ts b/src/runtimes/openai/generated/v2/CollabAgentToolCallStatus.ts deleted file mode 100644 index 2309bb8..0000000 --- a/src/runtimes/openai/generated/v2/CollabAgentToolCallStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CollabAgentToolCallStatus = "inProgress" | "completed" | "failed" | "interrupted"; diff --git a/src/runtimes/openai/generated/v2/CollaborationModeListParams.ts b/src/runtimes/openai/generated/v2/CollaborationModeListParams.ts deleted file mode 100644 index 37e8f79..0000000 --- a/src/runtimes/openai/generated/v2/CollaborationModeListParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - list collaboration mode presets. - */ -export type CollaborationModeListParams = Record; diff --git a/src/runtimes/openai/generated/v2/CollaborationModeListResponse.ts b/src/runtimes/openai/generated/v2/CollaborationModeListResponse.ts deleted file mode 100644 index 675f02d..0000000 --- a/src/runtimes/openai/generated/v2/CollaborationModeListResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CollaborationModeMask } from "./CollaborationModeMask"; - -/** - * EXPERIMENTAL - collaboration mode presets response. - */ -export type CollaborationModeListResponse = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/CollaborationModeMask.ts b/src/runtimes/openai/generated/v2/CollaborationModeMask.ts deleted file mode 100644 index 0e30e08..0000000 --- a/src/runtimes/openai/generated/v2/CollaborationModeMask.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ModeKind } from "../ModeKind"; -import type { ReasoningEffort } from "../ReasoningEffort"; - -/** - * EXPERIMENTAL - collaboration mode preset metadata for clients. - */ -export type CollaborationModeMask = { - name: string; - mode: ModeKind | null; - model: string | null; - reasoning_effort: ReasoningEffort | null | null; -}; diff --git a/src/runtimes/openai/generated/v2/CommandAction.ts b/src/runtimes/openai/generated/v2/CommandAction.ts deleted file mode 100644 index b88e953..0000000 --- a/src/runtimes/openai/generated/v2/CommandAction.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LegacyAppPathString } from "../LegacyAppPathString"; - -export type CommandAction = - | { type: "read"; command: string; name: string; path: LegacyAppPathString } - | { type: "listFiles"; command: string; path: string | null } - | { type: "search"; command: string; query: string | null; path: string | null } - | { type: "unknown"; command: string }; diff --git a/src/runtimes/openai/generated/v2/CommandExecOutputDeltaNotification.ts b/src/runtimes/openai/generated/v2/CommandExecOutputDeltaNotification.ts deleted file mode 100644 index 988ccc9..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecOutputDeltaNotification.ts +++ /dev/null @@ -1,31 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandExecOutputStream } from "./CommandExecOutputStream"; - -/** - * Base64-encoded output chunk emitted for a streaming `command/exec` request. - * - * These notifications are connection-scoped. If the originating connection - * closes, the server terminates the process. - */ -export type CommandExecOutputDeltaNotification = { - /** - * Client-supplied, connection-scoped `processId` from the original - * `command/exec` request. - */ - processId: string; - /** - * Output stream for this chunk. - */ - stream: CommandExecOutputStream; - /** - * Base64-encoded output bytes. - */ - deltaBase64: string; - /** - * `true` on the final streamed chunk for a stream when `outputBytesCap` - * truncated later output on that stream. - */ - capReached: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecOutputStream.ts b/src/runtimes/openai/generated/v2/CommandExecOutputStream.ts deleted file mode 100644 index a8c5b66..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecOutputStream.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Stream label for `command/exec/outputDelta` notifications. - */ -export type CommandExecOutputStream = "stdout" | "stderr"; diff --git a/src/runtimes/openai/generated/v2/CommandExecParams.ts b/src/runtimes/openai/generated/v2/CommandExecParams.ts deleted file mode 100644 index 1f6cebb..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecParams.ts +++ /dev/null @@ -1,106 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; -import type { SandboxPolicy } from "./SandboxPolicy"; - -/** - * Run a standalone command (argv vector) in the server sandbox without - * creating a thread or turn. - * - * The final `command/exec` response is deferred until the process exits and is - * sent only after all `command/exec/outputDelta` notifications for that - * connection have been emitted. - */ -export type CommandExecParams = { - /** - * Command argv vector. Empty arrays are rejected. - */ - command: Array; - /** - * Optional client-supplied, connection-scoped process id. - * - * Required for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up - * `command/exec/write`, `command/exec/resize`, and - * `command/exec/terminate` calls. When omitted, buffered execution gets an - * internal id that is not exposed to the client. - */ - processId?: string | null; - /** - * Enable PTY mode. - * - * This implies `streamStdin` and `streamStdoutStderr`. - */ - tty?: boolean; - /** - * Allow follow-up `command/exec/write` requests to write stdin bytes. - * - * Requires a client-supplied `processId`. - */ - streamStdin?: boolean; - /** - * Stream stdout/stderr via `command/exec/outputDelta` notifications. - * - * Streamed bytes are not duplicated into the final response and require a - * client-supplied `processId`. - */ - streamStdoutStderr?: boolean; - /** - * Optional per-stream stdout/stderr capture cap in bytes. - * - * When omitted, the server default applies. Cannot be combined with - * `disableOutputCap`. - */ - outputBytesCap?: number | null; - /** - * Disable stdout/stderr capture truncation for this request. - * - * Cannot be combined with `outputBytesCap`. - */ - disableOutputCap?: boolean; - /** - * Disable the timeout entirely for this request. - * - * Cannot be combined with `timeoutMs`. - */ - disableTimeout?: boolean; - /** - * Optional timeout in milliseconds. - * - * When omitted, the server default applies. Cannot be combined with - * `disableTimeout`. - */ - timeoutMs?: number | null; - /** - * Optional working directory. Defaults to the server cwd. - */ - cwd?: string | null; - /** - * Optional environment overrides merged into the server-computed - * environment. - * - * Matching names override inherited values. Set a key to `null` to unset - * an inherited variable. - */ - env?: { [key in string]?: string | null } | null; - /** - * Optional initial PTY size in character cells. Only valid when `tty` is - * true. - */ - size?: CommandExecTerminalSize | null; - /** - * Optional sandbox policy for this command. - * - * Uses the same shape as thread/turn execution sandbox configuration and - * defaults to the user's configured policy when omitted. Cannot be - * combined with `permissionProfile`. - */ - sandboxPolicy?: SandboxPolicy | null; - /** - * Optional active permissions profile id for this command. - * - * Defaults to the user's configured permissions when omitted. Cannot be - * combined with `sandboxPolicy`. - */ - permissionProfile?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecResizeParams.ts b/src/runtimes/openai/generated/v2/CommandExecResizeParams.ts deleted file mode 100644 index 5a29036..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecResizeParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; - -/** - * Resize a running PTY-backed `command/exec` session. - */ -export type CommandExecResizeParams = { - /** - * Client-supplied, connection-scoped `processId` from the original - * `command/exec` request. - */ - processId: string; - /** - * New PTY size in character cells. - */ - size: CommandExecTerminalSize; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecResizeResponse.ts b/src/runtimes/openai/generated/v2/CommandExecResizeResponse.ts deleted file mode 100644 index 7b7f2be..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecResizeResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Empty success response for `command/exec/resize`. - */ -export type CommandExecResizeResponse = Record; diff --git a/src/runtimes/openai/generated/v2/CommandExecResponse.ts b/src/runtimes/openai/generated/v2/CommandExecResponse.ts deleted file mode 100644 index caa53bd..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecResponse.ts +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Final buffered result for `command/exec`. - */ -export type CommandExecResponse = { - /** - * Process exit code. - */ - exitCode: number; - /** - * Buffered stdout capture. - * - * Empty when stdout was streamed via `command/exec/outputDelta`. - */ - stdout: string; - /** - * Buffered stderr capture. - * - * Empty when stderr was streamed via `command/exec/outputDelta`. - */ - stderr: string; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecTerminalSize.ts b/src/runtimes/openai/generated/v2/CommandExecTerminalSize.ts deleted file mode 100644 index 5d409e0..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecTerminalSize.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * PTY size in character cells for `command/exec` PTY sessions. - */ -export type CommandExecTerminalSize = { - /** - * Terminal height in character cells. - */ - rows: number; - /** - * Terminal width in character cells. - */ - cols: number; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecTerminateParams.ts b/src/runtimes/openai/generated/v2/CommandExecTerminateParams.ts deleted file mode 100644 index 43cbaab..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecTerminateParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Terminate a running `command/exec` session. - */ -export type CommandExecTerminateParams = { - /** - * Client-supplied, connection-scoped `processId` from the original - * `command/exec` request. - */ - processId: string; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecTerminateResponse.ts b/src/runtimes/openai/generated/v2/CommandExecTerminateResponse.ts deleted file mode 100644 index dc6371f..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecTerminateResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Empty success response for `command/exec/terminate`. - */ -export type CommandExecTerminateResponse = Record; diff --git a/src/runtimes/openai/generated/v2/CommandExecWriteParams.ts b/src/runtimes/openai/generated/v2/CommandExecWriteParams.ts deleted file mode 100644 index 960fadc..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecWriteParams.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Write stdin bytes to a running `command/exec` session, close stdin, or - * both. - */ -export type CommandExecWriteParams = { - /** - * Client-supplied, connection-scoped `processId` from the original - * `command/exec` request. - */ - processId: string; - /** - * Optional base64-encoded stdin bytes to write. - */ - deltaBase64?: string | null; - /** - * Close stdin after writing `deltaBase64`, if present. - */ - closeStdin?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecWriteResponse.ts b/src/runtimes/openai/generated/v2/CommandExecWriteResponse.ts deleted file mode 100644 index 6dbbddf..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecWriteResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Empty success response for `command/exec/write`. - */ -export type CommandExecWriteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionApprovalDecision.ts b/src/runtimes/openai/generated/v2/CommandExecutionApprovalDecision.ts deleted file mode 100644 index 79256f8..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecutionApprovalDecision.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; -import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; - -export type CommandExecutionApprovalDecision = - | "accept" - | "acceptForSession" - | { acceptWithExecpolicyAmendment: { execpolicy_amendment: ExecPolicyAmendment } } - | { applyNetworkPolicyAmendment: { network_policy_amendment: NetworkPolicyAmendment } } - | "decline" - | "cancel"; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionApprovalKind.ts b/src/runtimes/openai/generated/v2/CommandExecutionApprovalKind.ts deleted file mode 100644 index f94eac8..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecutionApprovalKind.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Distinguishes a command approval from input sent to an existing terminal. - */ -export type CommandExecutionApprovalKind = "command" | "writeStdin"; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionOutputDeltaNotification.ts b/src/runtimes/openai/generated/v2/CommandExecutionOutputDeltaNotification.ts deleted file mode 100644 index c476d44..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecutionOutputDeltaNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CommandExecutionOutputDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalParams.ts b/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalParams.ts deleted file mode 100644 index 836d948..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalParams.ts +++ /dev/null @@ -1,76 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LegacyAppPathString } from "../LegacyAppPathString"; -import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; -import type { CommandAction } from "./CommandAction"; -import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; -import type { CommandExecutionApprovalKind } from "./CommandExecutionApprovalKind"; -import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; -import type { NetworkApprovalContext } from "./NetworkApprovalContext"; -import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; - -export type CommandExecutionRequestApprovalParams = { - /** - * Kind of action under review. Defaults to `command` for older servers. - */ - kind: CommandExecutionApprovalKind; - threadId: string; - turnId: string; - itemId: string; - /** - * Unix timestamp (in milliseconds) when this approval request started. - */ - startedAtMs: number; - /** - * Unique identifier for this specific approval callback. - * - * For regular shell/unified_exec approvals, this is null. - * - * For zsh-exec-bridge subcommand approvals, multiple callbacks can belong to - * one parent `itemId`, so `approvalId` is a distinct opaque callback id - * (a UUID) used to disambiguate routing. - * Stdin approvals also use a distinct callback id; inspect `kind` to distinguish them. - */ - approvalId?: string | null; - /** - * Environment in which the command will run. - */ - environmentId: string | null; - /** - * Optional explanatory reason (e.g. request for network access). - */ - reason?: string | null; - /** - * Optional context for a managed-network approval prompt. - */ - networkApprovalContext?: NetworkApprovalContext | null; - /** - * The command to be executed. - */ - command?: string | null; - /** - * The command's working directory. - */ - cwd?: LegacyAppPathString | null; - /** - * Best-effort parsed command actions for friendly display. - */ - commandActions?: Array | null; - /** - * Optional additional permissions requested for this command. - */ - additionalPermissions?: AdditionalPermissionProfile | null; - /** - * Optional proposed execpolicy amendment to allow similar commands without prompting. - */ - proposedExecpolicyAmendment?: ExecPolicyAmendment | null; - /** - * Optional proposed network policy amendments (allow/deny host) for future requests. - */ - proposedNetworkPolicyAmendments?: Array | null; - /** - * Ordered list of decisions the client may present for this prompt. - */ - availableDecisions?: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalResponse.ts b/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalResponse.ts deleted file mode 100644 index b4b31d0..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecutionRequestApprovalResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; - -export type CommandExecutionRequestApprovalResponse = { - decision: CommandExecutionApprovalDecision; -}; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionSource.ts b/src/runtimes/openai/generated/v2/CommandExecutionSource.ts deleted file mode 100644 index 790b65b..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecutionSource.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CommandExecutionSource = - | "agent" - | "userShell" - | "unifiedExecStartup" - | "unifiedExecInteraction"; diff --git a/src/runtimes/openai/generated/v2/CommandExecutionStatus.ts b/src/runtimes/openai/generated/v2/CommandExecutionStatus.ts deleted file mode 100644 index c58b3cc..0000000 --- a/src/runtimes/openai/generated/v2/CommandExecutionStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CommandExecutionStatus = "inProgress" | "completed" | "failed" | "declined"; diff --git a/src/runtimes/openai/generated/v2/CommandMigration.ts b/src/runtimes/openai/generated/v2/CommandMigration.ts deleted file mode 100644 index 8f2bee0..0000000 --- a/src/runtimes/openai/generated/v2/CommandMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CommandMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/ComputerUseConfig.ts b/src/runtimes/openai/generated/v2/ComputerUseConfig.ts deleted file mode 100644 index 266f935..0000000 --- a/src/runtimes/openai/generated/v2/ComputerUseConfig.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; -import type { ComputerUseMacosConfig } from "./ComputerUseMacosConfig"; -import type { ComputerUseWindowsConfig } from "./ComputerUseWindowsConfig"; - -export type ComputerUseConfig = { - default_app_access: AllowDenyRequirement | null; - macos: ComputerUseMacosConfig | null; - windows: ComputerUseWindowsConfig | null; -}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseMacosConfig.ts b/src/runtimes/openai/generated/v2/ComputerUseMacosConfig.ts deleted file mode 100644 index cfbf6d3..0000000 --- a/src/runtimes/openai/generated/v2/ComputerUseMacosConfig.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; - -export type ComputerUseMacosConfig = { - bundle_ids: { [key in string]?: AllowDenyRequirement } | null; -}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseMacosRequirements.ts b/src/runtimes/openai/generated/v2/ComputerUseMacosRequirements.ts deleted file mode 100644 index 6b329e2..0000000 --- a/src/runtimes/openai/generated/v2/ComputerUseMacosRequirements.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; - -export type ComputerUseMacosRequirements = { - bundleIds: { [key in string]?: AllowDenyRequirement } | null; -}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseRequirements.ts b/src/runtimes/openai/generated/v2/ComputerUseRequirements.ts deleted file mode 100644 index bf7d9bd..0000000 --- a/src/runtimes/openai/generated/v2/ComputerUseRequirements.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; -import type { ComputerUseMacosRequirements } from "./ComputerUseMacosRequirements"; -import type { ComputerUseWindowsRequirements } from "./ComputerUseWindowsRequirements"; - -export type ComputerUseRequirements = { - allowLockedComputerUse: boolean | null; - allowPersistentApproval: boolean | null; - defaultAppAccess: AllowDenyRequirement | null; - macos: ComputerUseMacosRequirements | null; - windows: ComputerUseWindowsRequirements | null; -}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseWindowsConfig.ts b/src/runtimes/openai/generated/v2/ComputerUseWindowsConfig.ts deleted file mode 100644 index f07511c..0000000 --- a/src/runtimes/openai/generated/v2/ComputerUseWindowsConfig.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; -import type { ComputerUseWindowsExeConfig } from "./ComputerUseWindowsExeConfig"; - -export type ComputerUseWindowsConfig = { - aumids: { [key in string]?: AllowDenyRequirement } | null; - exes: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseWindowsExeConfig.ts b/src/runtimes/openai/generated/v2/ComputerUseWindowsExeConfig.ts deleted file mode 100644 index 00569e6..0000000 --- a/src/runtimes/openai/generated/v2/ComputerUseWindowsExeConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; - -export type ComputerUseWindowsExeConfig = { - publisher_name: string; - product_name: string; - binary_name: string | null; - access: AllowDenyRequirement; -}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseWindowsExeRequirement.ts b/src/runtimes/openai/generated/v2/ComputerUseWindowsExeRequirement.ts deleted file mode 100644 index 191fe73..0000000 --- a/src/runtimes/openai/generated/v2/ComputerUseWindowsExeRequirement.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; - -export type ComputerUseWindowsExeRequirement = { - publisherName: string; - productName: string; - binaryName: string | null; - access: AllowDenyRequirement; -}; diff --git a/src/runtimes/openai/generated/v2/ComputerUseWindowsRequirements.ts b/src/runtimes/openai/generated/v2/ComputerUseWindowsRequirements.ts deleted file mode 100644 index 9ce7c33..0000000 --- a/src/runtimes/openai/generated/v2/ComputerUseWindowsRequirements.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowDenyRequirement } from "./AllowDenyRequirement"; -import type { ComputerUseWindowsExeRequirement } from "./ComputerUseWindowsExeRequirement"; - -export type ComputerUseWindowsRequirements = { - aumids: { [key in string]?: AllowDenyRequirement } | null; - exes: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/Config.ts b/src/runtimes/openai/generated/v2/Config.ts deleted file mode 100644 index 5fd1848..0000000 --- a/src/runtimes/openai/generated/v2/Config.ts +++ /dev/null @@ -1,61 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AutoCompactTokenLimitScope } from "../AutoCompactTokenLimitScope"; -import type { ForcedLoginMethod } from "../ForcedLoginMethod"; -import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ReasoningSummary } from "../ReasoningSummary"; -import type { Verbosity } from "../Verbosity"; -import type { WebSearchMode } from "../WebSearchMode"; -import type { JsonValue } from "../serde_json/JsonValue"; -import type { AnalyticsConfig } from "./AnalyticsConfig"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AppsConfig } from "./AppsConfig"; -import type { AskForApproval } from "./AskForApproval"; -import type { BrowserUseConfig } from "./BrowserUseConfig"; -import type { ComputerUseConfig } from "./ComputerUseConfig"; -import type { ForcedChatgptWorkspaceIds } from "./ForcedChatgptWorkspaceIds"; -import type { SandboxMode } from "./SandboxMode"; -import type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; -import type { ToolsV2 } from "./ToolsV2"; - -export type Config = { - model: string | null; - review_model: string | null; - model_context_window: bigint | null; - model_auto_compact_token_limit: bigint | null; - model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope | null; - model_provider: string | null; - approval_policy: AskForApproval | null; - /** - * [UNSTABLE] Optional default for where approval requests are routed for - * review. - */ - approvals_reviewer: ApprovalsReviewer | null; - sandbox_mode: SandboxMode | null; - sandbox_workspace_write: SandboxWorkspaceWrite | null; - forced_chatgpt_workspace_id: ForcedChatgptWorkspaceIds | null; - forced_login_method: ForcedLoginMethod | null; - web_search: WebSearchMode | null; - tools: ToolsV2 | null; - instructions: string | null; - developer_instructions: string | null; - compact_prompt: string | null; - model_reasoning_effort: ReasoningEffort | null; - model_reasoning_summary: ReasoningSummary | null; - model_verbosity: Verbosity | null; - service_tier: string | null; - analytics: AnalyticsConfig | null; - apps: AppsConfig | null; - browser_use: BrowserUseConfig | null; - computer_use: ComputerUseConfig | null; - desktop: { [key in string]?: JsonValue } | null; -} & { - [key in string]?: - | number - | string - | boolean - | Array - | { [key in string]?: JsonValue } - | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConfigBatchWriteParams.ts b/src/runtimes/openai/generated/v2/ConfigBatchWriteParams.ts deleted file mode 100644 index 91eff21..0000000 --- a/src/runtimes/openai/generated/v2/ConfigBatchWriteParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfigEdit } from "./ConfigEdit"; - -export type ConfigBatchWriteParams = { - edits: Array; - /** - * Path to the config file to write; defaults to the user's `config.toml` when omitted. - */ - filePath?: string | null; - expectedVersion?: string | null; - /** - * When true, hot-reload updated runtime settings into loaded threads after writing. - * Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and - * personality defaults are not reloaded. - */ - reloadUserConfig?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ConfigEdit.ts b/src/runtimes/openai/generated/v2/ConfigEdit.ts deleted file mode 100644 index b13e75b..0000000 --- a/src/runtimes/openai/generated/v2/ConfigEdit.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; -import type { MergeStrategy } from "./MergeStrategy"; - -export type ConfigEdit = { keyPath: string; value: JsonValue; mergeStrategy: MergeStrategy }; diff --git a/src/runtimes/openai/generated/v2/ConfigLayer.ts b/src/runtimes/openai/generated/v2/ConfigLayer.ts deleted file mode 100644 index e549226..0000000 --- a/src/runtimes/openai/generated/v2/ConfigLayer.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; -import type { ConfigLayerSource } from "./ConfigLayerSource"; - -export type ConfigLayer = { - name: ConfigLayerSource; - version: string; - config: JsonValue; - disabledReason: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConfigLayerMetadata.ts b/src/runtimes/openai/generated/v2/ConfigLayerMetadata.ts deleted file mode 100644 index 989a5a6..0000000 --- a/src/runtimes/openai/generated/v2/ConfigLayerMetadata.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfigLayerSource } from "./ConfigLayerSource"; - -export type ConfigLayerMetadata = { name: ConfigLayerSource; version: string }; diff --git a/src/runtimes/openai/generated/v2/ConfigLayerSource.ts b/src/runtimes/openai/generated/v2/ConfigLayerSource.ts deleted file mode 100644 index 4c1b296..0000000 --- a/src/runtimes/openai/generated/v2/ConfigLayerSource.ts +++ /dev/null @@ -1,52 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type ConfigLayerSource = - | { - type: "packagedDefaults"; - /** - * Path to the packaged default configuration file. - */ - file: AbsolutePathBuf; - } - | { type: "mdm"; domain: string; key: string } - | { - type: "system"; - /** - * This is the path to the system config.toml file, though it is not - * guaranteed to exist. - */ - file: AbsolutePathBuf; - } - | { - type: "enterpriseManaged"; - /** - * Stable identifier for the delivered layer. - */ - id: string; - /** - * Admin-facing name for the delivered layer. This is surfaced in - * diagnostics so users know which cloud layer needs administrator - * attention. - */ - name: string; - } - | { - type: "user"; - /** - * This is the path to the user's config.toml file, though it is not - * guaranteed to exist. - */ - file: AbsolutePathBuf; - /** - * Name of the selected profile-v2 config layered on top of the base - * user config, when this layer represents one. - */ - profile: string | null; - } - | { type: "project"; dotCodexFolder: AbsolutePathBuf } - | { type: "sessionFlags" } - | { type: "legacyManagedConfigTomlFromFile"; file: AbsolutePathBuf } - | { type: "legacyManagedConfigTomlFromMdm" }; diff --git a/src/runtimes/openai/generated/v2/ConfigReadParams.ts b/src/runtimes/openai/generated/v2/ConfigReadParams.ts deleted file mode 100644 index 14de081..0000000 --- a/src/runtimes/openai/generated/v2/ConfigReadParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConfigReadParams = { - includeLayers?: boolean; - /** - * Optional working directory to resolve project config layers. If specified, - * return the effective config as seen from that directory (i.e., including any - * project layers between `cwd` and the project/repo root). - */ - cwd?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConfigReadResponse.ts b/src/runtimes/openai/generated/v2/ConfigReadResponse.ts deleted file mode 100644 index fcd4a88..0000000 --- a/src/runtimes/openai/generated/v2/ConfigReadResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Config } from "./Config"; -import type { ConfigLayer } from "./ConfigLayer"; -import type { ConfigLayerMetadata } from "./ConfigLayerMetadata"; - -export type ConfigReadResponse = { - config: Config; - origins: { [key in string]?: ConfigLayerMetadata }; - layers: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConfigRequirements.ts b/src/runtimes/openai/generated/v2/ConfigRequirements.ts deleted file mode 100644 index 9faeb64..0000000 --- a/src/runtimes/openai/generated/v2/ConfigRequirements.ts +++ /dev/null @@ -1,52 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PathUri } from "../PathUri"; -import type { WebSearchMode } from "../WebSearchMode"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AskForApproval } from "./AskForApproval"; -import type { AutoReviewRequirements } from "./AutoReviewRequirements"; -import type { BrowserUseRequirements } from "./BrowserUseRequirements"; -import type { CliAuthCredentialsStoreMode } from "./CliAuthCredentialsStoreMode"; -import type { ComputerUseRequirements } from "./ComputerUseRequirements"; -import type { FeedbackRequirements } from "./FeedbackRequirements"; -import type { InAppBrowserRequirements } from "./InAppBrowserRequirements"; -import type { ManagedHooksRequirements } from "./ManagedHooksRequirements"; -import type { ModelsRequirements } from "./ModelsRequirements"; -import type { NetworkRequirements } from "./NetworkRequirements"; -import type { ResidencyRequirement } from "./ResidencyRequirement"; -import type { SandboxMode } from "./SandboxMode"; -import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; - -export type ConfigRequirements = { - cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null; - chatgptBaseUrl: string | null; - additionalDeveloperInstructions: string | null; - allowedApprovalPolicies: Array | null; - allowedApprovalsReviewers: Array | null; - allowedSandboxModes: Array | null; - allowedWindowsSandboxImplementations: Array | null; - allowedPermissionProfiles: { [key in string]?: boolean } | null; - defaultPermissions: string | null; - allowedWebSearchModes: Array | null; - allowManagedHooksOnly: boolean | null; - allowBrowserAndComputerUse: boolean | null; - allowAppshots: boolean | null; - allowRemoteControl: boolean | null; - computerUse: ComputerUseRequirements | null; - browserUse: BrowserUseRequirements | null; - inAppBrowser: InAppBrowserRequirements | null; - featureRequirements: { [key in string]?: boolean } | null; - hooks: ManagedHooksRequirements | null; - enforceResidency: ResidencyRequirement | null; - network: NetworkRequirements | null; - autoReview: AutoReviewRequirements | null; - models: ModelsRequirements | null; - sqliteHome: PathUri | null; - logDir: PathUri | null; - modelCatalogJson: PathUri | null; - checkForUpdateOnStartup: boolean | null; - allowLoginShell: boolean | null; - feedback: FeedbackRequirements | null; - windowsSandboxPrivateDesktop: boolean | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConfigRequirementsReadResponse.ts b/src/runtimes/openai/generated/v2/ConfigRequirementsReadResponse.ts deleted file mode 100644 index 14e736c..0000000 --- a/src/runtimes/openai/generated/v2/ConfigRequirementsReadResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfigRequirements } from "./ConfigRequirements"; - -export type ConfigRequirementsReadResponse = { - /** - * Null if no requirements are configured (e.g. no requirements.toml/MDM entries). - */ - requirements: ConfigRequirements | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConfigValueWriteParams.ts b/src/runtimes/openai/generated/v2/ConfigValueWriteParams.ts deleted file mode 100644 index 34cd296..0000000 --- a/src/runtimes/openai/generated/v2/ConfigValueWriteParams.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; -import type { MergeStrategy } from "./MergeStrategy"; - -export type ConfigValueWriteParams = { - keyPath: string; - value: JsonValue; - mergeStrategy: MergeStrategy; - /** - * Path to the config file to write; defaults to the user's `config.toml` when omitted. - */ - filePath?: string | null; - expectedVersion?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConfigWarningNotification.ts b/src/runtimes/openai/generated/v2/ConfigWarningNotification.ts deleted file mode 100644 index 8efae84..0000000 --- a/src/runtimes/openai/generated/v2/ConfigWarningNotification.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TextRange } from "./TextRange"; - -export type ConfigWarningNotification = { - /** - * Concise summary of the warning. - */ - summary: string; - /** - * Optional extra guidance or error details. - */ - details: string | null; - /** - * Optional path to the config file that triggered the warning. - */ - path?: string; - /** - * Optional range for the error location inside the config file. - */ - range?: TextRange; -}; diff --git a/src/runtimes/openai/generated/v2/ConfigWriteResponse.ts b/src/runtimes/openai/generated/v2/ConfigWriteResponse.ts deleted file mode 100644 index 7c54ebc..0000000 --- a/src/runtimes/openai/generated/v2/ConfigWriteResponse.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { OverriddenMetadata } from "./OverriddenMetadata"; -import type { WriteStatus } from "./WriteStatus"; - -export type ConfigWriteResponse = { - status: WriteStatus; - version: string; - /** - * Canonical path to the config file that was written. - */ - filePath: AbsolutePathBuf; - overriddenMetadata: OverriddenMetadata | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConfiguredHookHandler.ts b/src/runtimes/openai/generated/v2/ConfiguredHookHandler.ts deleted file mode 100644 index 76e9468..0000000 --- a/src/runtimes/openai/generated/v2/ConfiguredHookHandler.ts +++ /dev/null @@ -1,31 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type ConfiguredHookHandler = - | { - type: "command"; - command: string; - commandWindows: string | null; - timeoutSec: bigint | null; - async: boolean; - statusMessage: string | null; - /** - * Approximate token threshold for spilling this hook's `additionalContext` to disk. - * `null` uses 2,500 tokens; `0` disables spilling for this hook. The threshold is - * evaluated against the original context; a spilled preview also includes recovery - * metadata. - */ - additionalContextLimit: number | null; - } - | { - type: "mcp_tool"; - server: string; - tool: string; - input: { [key in string]?: JsonValue }; - timeoutSec: bigint | null; - statusMessage: string | null; - } - | { type: "prompt" } - | { type: "agent" }; diff --git a/src/runtimes/openai/generated/v2/ConfiguredHookMatcherGroup.ts b/src/runtimes/openai/generated/v2/ConfiguredHookMatcherGroup.ts deleted file mode 100644 index 2da0a76..0000000 --- a/src/runtimes/openai/generated/v2/ConfiguredHookMatcherGroup.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfiguredHookHandler } from "./ConfiguredHookHandler"; - -export type ConfiguredHookMatcherGroup = { - matcher: string | null; - hooks: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ConnectorMetadata.ts b/src/runtimes/openai/generated/v2/ConnectorMetadata.ts deleted file mode 100644 index 9b324f7..0000000 --- a/src/runtimes/openai/generated/v2/ConnectorMetadata.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppToolSummary } from "./AppToolSummary"; - -/** - * EXPERIMENTAL - metadata returned by app/read. - */ -export type ConnectorMetadata = { - id: string; - name: string; - description: string | null; - iconUrl: string | null; - iconUrlDark: string | null; - distributionChannel: string | null; - installUrl: string | null; - pluginDisplayNames: Array; - toolSummaries: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditOutcome.ts b/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditOutcome.ts deleted file mode 100644 index bfe843e..0000000 --- a/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditOutcome.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConsumeAccountRateLimitResetCreditOutcome = - | "reset" - | "nothingToReset" - | "noCredit" - | "alreadyRedeemed"; diff --git a/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditParams.ts b/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditParams.ts deleted file mode 100644 index 2f52827..0000000 --- a/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditParams.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConsumeAccountRateLimitResetCreditParams = { - /** - * Identifies one logical reset attempt. A UUID is recommended; reuse the same value when - * retrying that attempt. - */ - idempotencyKey: string; - /** - * Opaque reset-credit identifier to redeem. When omitted, the backend selects the next - * available credit. - */ - creditId?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditResponse.ts b/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditResponse.ts deleted file mode 100644 index 455448e..0000000 --- a/src/runtimes/openai/generated/v2/ConsumeAccountRateLimitResetCreditResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConsumeAccountRateLimitResetCreditOutcome } from "./ConsumeAccountRateLimitResetCreditOutcome"; - -export type ConsumeAccountRateLimitResetCreditResponse = { - outcome: ConsumeAccountRateLimitResetCreditOutcome; -}; diff --git a/src/runtimes/openai/generated/v2/ContextCompactedNotification.ts b/src/runtimes/openai/generated/v2/ContextCompactedNotification.ts deleted file mode 100644 index bb68253..0000000 --- a/src/runtimes/openai/generated/v2/ContextCompactedNotification.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Deprecated: Use `ContextCompaction` item type instead. - */ -export type ContextCompactedNotification = { threadId: string; turnId: string }; diff --git a/src/runtimes/openai/generated/v2/CreditsSnapshot.ts b/src/runtimes/openai/generated/v2/CreditsSnapshot.ts deleted file mode 100644 index dd5e746..0000000 --- a/src/runtimes/openai/generated/v2/CreditsSnapshot.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CreditsSnapshot = { hasCredits: boolean; unlimited: boolean; balance: string | null }; diff --git a/src/runtimes/openai/generated/v2/CurrentTimeReadParams.ts b/src/runtimes/openai/generated/v2/CurrentTimeReadParams.ts deleted file mode 100644 index 3c8c459..0000000 --- a/src/runtimes/openai/generated/v2/CurrentTimeReadParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CurrentTimeReadParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/DeprecationNoticeNotification.ts b/src/runtimes/openai/generated/v2/DeprecationNoticeNotification.ts deleted file mode 100644 index 54f3d08..0000000 --- a/src/runtimes/openai/generated/v2/DeprecationNoticeNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DeprecationNoticeNotification = { - /** - * Concise summary of what is deprecated. - */ - summary: string; - /** - * Optional extra guidance, such as migration steps or rationale. - */ - details: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/DesktopOnboardingEntrypoint.ts b/src/runtimes/openai/generated/v2/DesktopOnboardingEntrypoint.ts deleted file mode 100644 index 75a7321..0000000 --- a/src/runtimes/openai/generated/v2/DesktopOnboardingEntrypoint.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DesktopOnboardingEntrypoint = "life_sciences"; diff --git a/src/runtimes/openai/generated/v2/DynamicToolCallOutputContentItem.ts b/src/runtimes/openai/generated/v2/DynamicToolCallOutputContentItem.ts deleted file mode 100644 index 16f6769..0000000 --- a/src/runtimes/openai/generated/v2/DynamicToolCallOutputContentItem.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DynamicToolCallOutputContentItem = - | { type: "inputText"; text: string } - | { type: "inputImage"; imageUrl: string } - | { type: "inputAudio"; audioUrl: string }; diff --git a/src/runtimes/openai/generated/v2/DynamicToolCallParams.ts b/src/runtimes/openai/generated/v2/DynamicToolCallParams.ts deleted file mode 100644 index 76d89b5..0000000 --- a/src/runtimes/openai/generated/v2/DynamicToolCallParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type DynamicToolCallParams = { - threadId: string; - turnId: string; - callId: string; - namespace: string | null; - tool: string; - arguments: JsonValue; -}; diff --git a/src/runtimes/openai/generated/v2/DynamicToolCallResponse.ts b/src/runtimes/openai/generated/v2/DynamicToolCallResponse.ts deleted file mode 100644 index 51b6d6c..0000000 --- a/src/runtimes/openai/generated/v2/DynamicToolCallResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; - -export type DynamicToolCallResponse = { - contentItems: Array; - success: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/DynamicToolCallStatus.ts b/src/runtimes/openai/generated/v2/DynamicToolCallStatus.ts deleted file mode 100644 index 04f44ec..0000000 --- a/src/runtimes/openai/generated/v2/DynamicToolCallStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DynamicToolCallStatus = "inProgress" | "completed" | "failed"; diff --git a/src/runtimes/openai/generated/v2/EnvironmentAddParams.ts b/src/runtimes/openai/generated/v2/EnvironmentAddParams.ts deleted file mode 100644 index 005fdc9..0000000 --- a/src/runtimes/openai/generated/v2/EnvironmentAddParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type EnvironmentAddParams = { - environmentId: string; - execServerUrl: string; - /** - * Optional WebSocket connection timeout. The server default applies when omitted. - */ - connectTimeoutMs?: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/EnvironmentAddResponse.ts b/src/runtimes/openai/generated/v2/EnvironmentAddResponse.ts deleted file mode 100644 index 5b0a2da..0000000 --- a/src/runtimes/openai/generated/v2/EnvironmentAddResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type EnvironmentAddResponse = Record; diff --git a/src/runtimes/openai/generated/v2/EnvironmentConnectionNotification.ts b/src/runtimes/openai/generated/v2/EnvironmentConnectionNotification.ts deleted file mode 100644 index 1c62da1..0000000 --- a/src/runtimes/openai/generated/v2/EnvironmentConnectionNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type EnvironmentConnectionNotification = { threadId: string; environmentId: string }; diff --git a/src/runtimes/openai/generated/v2/EnvironmentInfoParams.ts b/src/runtimes/openai/generated/v2/EnvironmentInfoParams.ts deleted file mode 100644 index e52a6fd..0000000 --- a/src/runtimes/openai/generated/v2/EnvironmentInfoParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type EnvironmentInfoParams = { environmentId: string }; diff --git a/src/runtimes/openai/generated/v2/EnvironmentInfoResponse.ts b/src/runtimes/openai/generated/v2/EnvironmentInfoResponse.ts deleted file mode 100644 index f830eae..0000000 --- a/src/runtimes/openai/generated/v2/EnvironmentInfoResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PathUri } from "../PathUri"; -import type { EnvironmentShellInfo } from "./EnvironmentShellInfo"; - -export type EnvironmentInfoResponse = { - shell: EnvironmentShellInfo; - /** - * Default working directory reported by the environment, as a canonical file URI. - */ - cwd: PathUri | null; -}; diff --git a/src/runtimes/openai/generated/v2/EnvironmentShellInfo.ts b/src/runtimes/openai/generated/v2/EnvironmentShellInfo.ts deleted file mode 100644 index b89a421..0000000 --- a/src/runtimes/openai/generated/v2/EnvironmentShellInfo.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type EnvironmentShellInfo = { - /** - * Stable shell name, for example `zsh`, `bash`, `powershell`, `sh`, or `cmd`. - */ - name: string; - /** - * Target-native shell executable path or command name. - */ - path: string; -}; diff --git a/src/runtimes/openai/generated/v2/EnvironmentStatusKind.ts b/src/runtimes/openai/generated/v2/EnvironmentStatusKind.ts deleted file mode 100644 index cc535a2..0000000 --- a/src/runtimes/openai/generated/v2/EnvironmentStatusKind.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Current status observed by app-server without starting or recovering an environment. - * - * For a currently ready remote environment, app-server asks the existing - * exec-server connection for `environment/status` without allowing recovery. - */ -export type EnvironmentStatusKind = "ready" | "pending" | "disconnected" | "unknown"; diff --git a/src/runtimes/openai/generated/v2/EnvironmentStatusParams.ts b/src/runtimes/openai/generated/v2/EnvironmentStatusParams.ts deleted file mode 100644 index 9d3b500..0000000 --- a/src/runtimes/openai/generated/v2/EnvironmentStatusParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for reading the current status of one configured environment. - */ -export type EnvironmentStatusParams = { - /** - * Environment id to inspect. - */ - environmentId: string; -}; diff --git a/src/runtimes/openai/generated/v2/EnvironmentStatusResponse.ts b/src/runtimes/openai/generated/v2/EnvironmentStatusResponse.ts deleted file mode 100644 index fd7251a..0000000 --- a/src/runtimes/openai/generated/v2/EnvironmentStatusResponse.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { EnvironmentStatusKind } from "./EnvironmentStatusKind"; - -/** - * Current status for the requested environment. - */ -export type EnvironmentStatusResponse = { - /** - * Current status observed without starting or recovering the environment. - */ - status: EnvironmentStatusKind; - /** - * Human-readable detail for `disconnected` and `unknown`; omitted for other statuses. - */ - error?: string; -}; diff --git a/src/runtimes/openai/generated/v2/ErrorNotification.ts b/src/runtimes/openai/generated/v2/ErrorNotification.ts deleted file mode 100644 index 220d462..0000000 --- a/src/runtimes/openai/generated/v2/ErrorNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TurnError } from "./TurnError"; - -export type ErrorNotification = { - error: TurnError; - willRetry: boolean; - threadId: string; - turnId: string; -}; diff --git a/src/runtimes/openai/generated/v2/ExecPolicyAmendment.ts b/src/runtimes/openai/generated/v2/ExecPolicyAmendment.ts deleted file mode 100644 index e893dd4..0000000 --- a/src/runtimes/openai/generated/v2/ExecPolicyAmendment.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExecPolicyAmendment = Array; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeature.ts b/src/runtimes/openai/generated/v2/ExperimentalFeature.ts deleted file mode 100644 index 6c4d048..0000000 --- a/src/runtimes/openai/generated/v2/ExperimentalFeature.ts +++ /dev/null @@ -1,38 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExperimentalFeatureStage } from "./ExperimentalFeatureStage"; - -export type ExperimentalFeature = { - /** - * Stable key used in config.toml and CLI flag toggles. - */ - name: string; - /** - * Lifecycle stage of this feature flag. - */ - stage: ExperimentalFeatureStage; - /** - * User-facing display name shown in the experimental features UI. - * Null when this feature is not in beta. - */ - displayName: string | null; - /** - * Short summary describing what the feature does. - * Null when this feature is not in beta. - */ - description: string | null; - /** - * Announcement copy shown to users when the feature is introduced. - * Null when this feature is not in beta. - */ - announcement: string | null; - /** - * Whether this feature is currently enabled in the loaded config. - */ - enabled: boolean; - /** - * Whether this feature is enabled by default. - */ - defaultEnabled: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetParams.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetParams.ts deleted file mode 100644 index de388e3..0000000 --- a/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExperimentalFeatureEnablementSetParams = { - /** - * Process-wide runtime feature enablement keyed by canonical feature name. - * - * Only named features are updated. Omitted features are left unchanged. - * Send an empty map for a no-op. - */ - enablement: { [key in string]?: boolean }; -}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetResponse.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetResponse.ts deleted file mode 100644 index e3349d1..0000000 --- a/src/runtimes/openai/generated/v2/ExperimentalFeatureEnablementSetResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExperimentalFeatureEnablementSetResponse = { - /** - * Feature enablement entries updated by this request. - */ - enablement: { [key in string]?: boolean }; -}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureListParams.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureListParams.ts deleted file mode 100644 index 3d55d59..0000000 --- a/src/runtimes/openai/generated/v2/ExperimentalFeatureListParams.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExperimentalFeatureListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a reasonable server-side value. - */ - limit?: number | null; - /** - * Optional loaded thread id. Pass this when showing feature state for an - * existing thread so enablement is computed from that thread's refreshed - * config, including project-local config for the thread's cwd. - */ - threadId?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureListResponse.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureListResponse.ts deleted file mode 100644 index 37c64b6..0000000 --- a/src/runtimes/openai/generated/v2/ExperimentalFeatureListResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExperimentalFeature } from "./ExperimentalFeature"; - -export type ExperimentalFeatureListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ExperimentalFeatureStage.ts b/src/runtimes/openai/generated/v2/ExperimentalFeatureStage.ts deleted file mode 100644 index b72828d..0000000 --- a/src/runtimes/openai/generated/v2/ExperimentalFeatureStage.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExperimentalFeatureStage = - | "beta" - | "underDevelopment" - | "stable" - | "deprecated" - | "removed"; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectParams.ts deleted file mode 100644 index eea4470..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectParams.ts +++ /dev/null @@ -1,31 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentConfigDetectParams = { - /** - * If true, include detection under the user's home directory. - */ - includeHome?: boolean; - /** - * Zero or more working directories to include for repo-scoped detection. - */ - cwds?: Array | null; - /** - * Maximum age in days for detected sessions. Missing values use the default limit. - */ - maxSessionAgeDays?: number | null; - /** - * Maximum number of sessions to detect. Missing values use the default limit. - */ - maxSessions?: number | null; - /** - * Deprecated field retained for compatibility. This field is ignored; use `migrationSource` - * to select the migration source. - */ - source?: string | null; - /** - * Optional migration-source selector. Missing or unrecognized values use the default source. - */ - migrationSource?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectResponse.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectResponse.ts deleted file mode 100644 index 2f7e40a..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigDetectResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; -import type { ExternalAgentDetectedConnectorCandidate } from "./ExternalAgentDetectedConnectorCandidate"; - -export type ExternalAgentConfigDetectResponse = { - items: Array; - connectors: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportCompletedNotification.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportCompletedNotification.ts deleted file mode 100644 index df10dfe..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportCompletedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigImportTypeResult } from "./ExternalAgentConfigImportTypeResult"; - -export type ExternalAgentConfigImportCompletedNotification = { - importId: string; - itemTypeResults: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.ts deleted file mode 100644 index bad6aa3..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoriesReadResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigImportHistory } from "./ExternalAgentConfigImportHistory"; -import type { ExternalAgentImportedConnectorCandidate } from "./ExternalAgentImportedConnectorCandidate"; - -export type ExternalAgentConfigImportHistoriesReadResponse = { - data: Array; - connectors: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistory.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistory.ts deleted file mode 100644 index 31c216f..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistory.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigImportItemTypeFailure } from "./ExternalAgentConfigImportItemTypeFailure"; -import type { ExternalAgentConfigImportItemTypeSuccess } from "./ExternalAgentConfigImportItemTypeSuccess"; - -export type ExternalAgentConfigImportHistory = { - importId: string; - providerId: string | null; - completedAtMs: bigint; - successes: Array; - failures: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordParams.ts deleted file mode 100644 index 21418af..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigImportHistoryRecordTypeResultParams } from "./ExternalAgentConfigImportHistoryRecordTypeResultParams"; - -export type ExternalAgentConfigImportHistoryRecordParams = { - /** - * Opaque provider identifier for the externally completed import. - */ - providerId: string; - /** - * Completed results grouped by imported item type. - */ - itemTypeResults: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordResponse.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordResponse.ts deleted file mode 100644 index ae38e7c..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentConfigImportHistoryRecordResponse = { importId: string }; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordSuccessParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordSuccessParams.ts deleted file mode 100644 index 5d644a3..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordSuccessParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; - -export type ExternalAgentConfigImportHistoryRecordSuccessParams = { - itemType: ExternalAgentConfigMigrationItemType; - cwd: string | null; - source: string | null; - target: string | null; - /** - * Original title for an imported session, when available. - */ - title?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordTypeResultParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordTypeResultParams.ts deleted file mode 100644 index 21f46d9..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportHistoryRecordTypeResultParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigImportHistoryRecordSuccessParams } from "./ExternalAgentConfigImportHistoryRecordSuccessParams"; -import type { ExternalAgentConfigImportItemTypeFailure } from "./ExternalAgentConfigImportItemTypeFailure"; -import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; - -export type ExternalAgentConfigImportHistoryRecordTypeResultParams = { - itemType: ExternalAgentConfigMigrationItemType; - successes: Array; - failures: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeFailure.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeFailure.ts deleted file mode 100644 index 8677fe9..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeFailure.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; - -export type ExternalAgentConfigImportItemTypeFailure = { - itemType: ExternalAgentConfigMigrationItemType; - errorType: string | null; - subErrorType: string | null; - failureStage: string; - message: string; - cwd: string | null; - source: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeSuccess.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeSuccess.ts deleted file mode 100644 index 6e26659..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportItemTypeSuccess.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; - -export type ExternalAgentConfigImportItemTypeSuccess = { - itemType: ExternalAgentConfigMigrationItemType; - cwd: string | null; - source: string | null; - target: string | null; - /** - * Original title for an imported session; null for other item types. - */ - title: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportParams.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportParams.ts deleted file mode 100644 index 8f14cfe..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; - -export type ExternalAgentConfigImportParams = { - migrationItems: Array; - /** - * Optional identifier for the product that initiated the import. - */ - source?: string | null; - /** - * Opaque provider identifier supplied by the caller for analytics attribution and import - * history display. This does not select the migration source. - */ - providerId?: string | null; - /** - * Migration-source selector used to produce the migration items. Pass the same value to - * detection and import; missing or unrecognized values use the default source. - */ - migrationSource?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportProgressNotification.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportProgressNotification.ts deleted file mode 100644 index 6ee91b0..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportProgressNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigImportTypeResult } from "./ExternalAgentConfigImportTypeResult"; - -export type ExternalAgentConfigImportProgressNotification = { - importId: string; - itemTypeResults: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportResponse.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportResponse.ts deleted file mode 100644 index de0b71a..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentConfigImportResponse = { importId: string }; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportTypeResult.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigImportTypeResult.ts deleted file mode 100644 index c8f701a..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigImportTypeResult.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigImportItemTypeFailure } from "./ExternalAgentConfigImportItemTypeFailure"; -import type { ExternalAgentConfigImportItemTypeSuccess } from "./ExternalAgentConfigImportItemTypeSuccess"; -import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; - -export type ExternalAgentConfigImportTypeResult = { - itemType: ExternalAgentConfigMigrationItemType; - successes: Array; - failures: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItem.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItem.ts deleted file mode 100644 index 3a58d35..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; -import type { MigrationDetails } from "./MigrationDetails"; - -export type ExternalAgentConfigMigrationItem = { - itemType: ExternalAgentConfigMigrationItemType; - description: string; - /** - * Null or empty means home-scoped migration; non-empty means repo-scoped migration. - */ - cwd: string | null; - details: MigrationDetails | null; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItemType.ts b/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItemType.ts deleted file mode 100644 index 5116e89..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentConfigMigrationItemType.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentConfigMigrationItemType = - | "AGENTS_MD" - | "CONFIG" - | "SKILLS" - | "PLUGINS" - | "MCP_SERVER_CONFIG" - | "SUBAGENTS" - | "HOOKS" - | "COMMANDS" - | "MEMORY" - | "SESSIONS"; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorCandidate.ts b/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorCandidate.ts deleted file mode 100644 index fb12fc4..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorCandidate.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentDetectedConnectorSource } from "./ExternalAgentDetectedConnectorSource"; - -export type ExternalAgentDetectedConnectorCandidate = { - name: string; - sessionCount: number; - source: ExternalAgentDetectedConnectorSource; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorSource.ts b/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorSource.ts deleted file mode 100644 index c4c608c..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentDetectedConnectorSource.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentDetectedConnectorSource = "remoteMcpServersConfig" | "sessionToolUse"; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorCandidate.ts b/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorCandidate.ts deleted file mode 100644 index d4f7321..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorCandidate.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentImportedConnectorSource } from "./ExternalAgentImportedConnectorSource"; - -export type ExternalAgentImportedConnectorCandidate = { - name: string; - sessionCount: number; - source: ExternalAgentImportedConnectorSource; -}; diff --git a/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorSource.ts b/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorSource.ts deleted file mode 100644 index 5398eb4..0000000 --- a/src/runtimes/openai/generated/v2/ExternalAgentImportedConnectorSource.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentImportedConnectorSource = "remoteMcpServersConfig"; diff --git a/src/runtimes/openai/generated/v2/FeedbackRequirements.ts b/src/runtimes/openai/generated/v2/FeedbackRequirements.ts deleted file mode 100644 index be44492..0000000 --- a/src/runtimes/openai/generated/v2/FeedbackRequirements.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FeedbackRequirements = { enabled: boolean | null }; diff --git a/src/runtimes/openai/generated/v2/FeedbackUploadParams.ts b/src/runtimes/openai/generated/v2/FeedbackUploadParams.ts deleted file mode 100644 index 21702cb..0000000 --- a/src/runtimes/openai/generated/v2/FeedbackUploadParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FeedbackUploadParams = { - classification: string; - reason?: string | null; - threadId?: string | null; - includeLogs?: boolean; - extraLogFiles?: Array | null; - tags?: { [key in string]?: string } | null; -}; diff --git a/src/runtimes/openai/generated/v2/FeedbackUploadResponse.ts b/src/runtimes/openai/generated/v2/FeedbackUploadResponse.ts deleted file mode 100644 index 6b2db64..0000000 --- a/src/runtimes/openai/generated/v2/FeedbackUploadResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FeedbackUploadResponse = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/FileChangeApprovalDecision.ts b/src/runtimes/openai/generated/v2/FileChangeApprovalDecision.ts deleted file mode 100644 index b74ba00..0000000 --- a/src/runtimes/openai/generated/v2/FileChangeApprovalDecision.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileChangeApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel"; diff --git a/src/runtimes/openai/generated/v2/FileChangeOutputDeltaNotification.ts b/src/runtimes/openai/generated/v2/FileChangeOutputDeltaNotification.ts deleted file mode 100644 index abd645c..0000000 --- a/src/runtimes/openai/generated/v2/FileChangeOutputDeltaNotification.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Deprecated legacy notification for `apply_patch` textual output. - * - * The server no longer emits this notification. - */ -export type FileChangeOutputDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; -}; diff --git a/src/runtimes/openai/generated/v2/FileChangePatchUpdatedNotification.ts b/src/runtimes/openai/generated/v2/FileChangePatchUpdatedNotification.ts deleted file mode 100644 index ccc7811..0000000 --- a/src/runtimes/openai/generated/v2/FileChangePatchUpdatedNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileUpdateChange } from "./FileUpdateChange"; - -export type FileChangePatchUpdatedNotification = { - threadId: string; - turnId: string; - itemId: string; - changes: Array; -}; diff --git a/src/runtimes/openai/generated/v2/FileChangeRequestApprovalParams.ts b/src/runtimes/openai/generated/v2/FileChangeRequestApprovalParams.ts deleted file mode 100644 index e01a715..0000000 --- a/src/runtimes/openai/generated/v2/FileChangeRequestApprovalParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileChangeRequestApprovalParams = { - threadId: string; - turnId: string; - itemId: string; - /** - * Unix timestamp (in milliseconds) when this approval request started. - */ - startedAtMs: number; - /** - * Optional explanatory reason (e.g. request for extra write access). - */ - reason?: string | null; - /** - * [UNSTABLE] When set, the agent is asking the user to allow writes under this root - * for the remainder of the session (unclear if this is honored today). - */ - grantRoot?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/FileChangeRequestApprovalResponse.ts b/src/runtimes/openai/generated/v2/FileChangeRequestApprovalResponse.ts deleted file mode 100644 index 621b6c4..0000000 --- a/src/runtimes/openai/generated/v2/FileChangeRequestApprovalResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision"; - -export type FileChangeRequestApprovalResponse = { decision: FileChangeApprovalDecision }; diff --git a/src/runtimes/openai/generated/v2/FileSystemAccessMode.ts b/src/runtimes/openai/generated/v2/FileSystemAccessMode.ts deleted file mode 100644 index 2dac727..0000000 --- a/src/runtimes/openai/generated/v2/FileSystemAccessMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileSystemAccessMode = "read" | "write" | "deny"; diff --git a/src/runtimes/openai/generated/v2/FileSystemPath.ts b/src/runtimes/openai/generated/v2/FileSystemPath.ts deleted file mode 100644 index b67e995..0000000 --- a/src/runtimes/openai/generated/v2/FileSystemPath.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LegacyAppPathString } from "../LegacyAppPathString"; -import type { FileSystemSpecialPath } from "./FileSystemSpecialPath"; - -export type FileSystemPath = - | { type: "path"; path: LegacyAppPathString } - | { type: "glob_pattern"; pattern: string } - | { type: "special"; value: FileSystemSpecialPath }; diff --git a/src/runtimes/openai/generated/v2/FileSystemSandboxEntry.ts b/src/runtimes/openai/generated/v2/FileSystemSandboxEntry.ts deleted file mode 100644 index e1a6224..0000000 --- a/src/runtimes/openai/generated/v2/FileSystemSandboxEntry.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileSystemAccessMode } from "./FileSystemAccessMode"; -import type { FileSystemPath } from "./FileSystemPath"; - -export type FileSystemSandboxEntry = { path: FileSystemPath; access: FileSystemAccessMode }; diff --git a/src/runtimes/openai/generated/v2/FileSystemSpecialPath.ts b/src/runtimes/openai/generated/v2/FileSystemSpecialPath.ts deleted file mode 100644 index 67a132a..0000000 --- a/src/runtimes/openai/generated/v2/FileSystemSpecialPath.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LegacyAppPathString } from "../LegacyAppPathString"; - -export type FileSystemSpecialPath = - | { kind: "root" } - | { kind: "minimal" } - | { kind: "project_roots"; subpath: LegacyAppPathString | null } - | { kind: "tmpdir" } - | { kind: "slash_tmp" } - | { kind: "unknown"; path: string; subpath: LegacyAppPathString | null }; diff --git a/src/runtimes/openai/generated/v2/FileUpdateChange.ts b/src/runtimes/openai/generated/v2/FileUpdateChange.ts deleted file mode 100644 index be1c4ce..0000000 --- a/src/runtimes/openai/generated/v2/FileUpdateChange.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PatchChangeKind } from "./PatchChangeKind"; - -export type FileUpdateChange = { path: string; kind: PatchChangeKind; diff: string }; diff --git a/src/runtimes/openai/generated/v2/ForcedChatgptWorkspaceIds.ts b/src/runtimes/openai/generated/v2/ForcedChatgptWorkspaceIds.ts deleted file mode 100644 index d0582c8..0000000 --- a/src/runtimes/openai/generated/v2/ForcedChatgptWorkspaceIds.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Backward-compatible API shape for ChatGPT workspace login restrictions. - */ -export type ForcedChatgptWorkspaceIds = string | Array; diff --git a/src/runtimes/openai/generated/v2/FsChangedNotification.ts b/src/runtimes/openai/generated/v2/FsChangedNotification.ts deleted file mode 100644 index fe64bdc..0000000 --- a/src/runtimes/openai/generated/v2/FsChangedNotification.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * Filesystem watch notification emitted for `fs/watch` subscribers. - */ -export type FsChangedNotification = { - /** - * Watch identifier previously provided to `fs/watch`. - */ - watchId: string; - /** - * File or directory paths associated with this event. - */ - changedPaths: Array; -}; diff --git a/src/runtimes/openai/generated/v2/FsCopyParams.ts b/src/runtimes/openai/generated/v2/FsCopyParams.ts deleted file mode 100644 index 2eec6d9..0000000 --- a/src/runtimes/openai/generated/v2/FsCopyParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * Copy a file or directory tree on the host filesystem. - */ -export type FsCopyParams = { - /** - * Absolute source path. - */ - sourcePath: AbsolutePathBuf; - /** - * Absolute destination path. - */ - destinationPath: AbsolutePathBuf; - /** - * Required for directory copies; ignored for file copies. - */ - recursive?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/FsCopyResponse.ts b/src/runtimes/openai/generated/v2/FsCopyResponse.ts deleted file mode 100644 index 3e3061a..0000000 --- a/src/runtimes/openai/generated/v2/FsCopyResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/copy`. - */ -export type FsCopyResponse = Record; diff --git a/src/runtimes/openai/generated/v2/FsCreateDirectoryParams.ts b/src/runtimes/openai/generated/v2/FsCreateDirectoryParams.ts deleted file mode 100644 index 334cfc7..0000000 --- a/src/runtimes/openai/generated/v2/FsCreateDirectoryParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * Create a directory on the host filesystem. - */ -export type FsCreateDirectoryParams = { - /** - * Absolute directory path to create. - */ - path: AbsolutePathBuf; - /** - * Whether parent directories should also be created. Defaults to `true`. - */ - recursive?: boolean | null; -}; diff --git a/src/runtimes/openai/generated/v2/FsCreateDirectoryResponse.ts b/src/runtimes/openai/generated/v2/FsCreateDirectoryResponse.ts deleted file mode 100644 index 5d251b7..0000000 --- a/src/runtimes/openai/generated/v2/FsCreateDirectoryResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/createDirectory`. - */ -export type FsCreateDirectoryResponse = Record; diff --git a/src/runtimes/openai/generated/v2/FsGetMetadataParams.ts b/src/runtimes/openai/generated/v2/FsGetMetadataParams.ts deleted file mode 100644 index f03e66f..0000000 --- a/src/runtimes/openai/generated/v2/FsGetMetadataParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * Request metadata for an absolute path. - */ -export type FsGetMetadataParams = { - /** - * Absolute path to inspect. - */ - path: AbsolutePathBuf; -}; diff --git a/src/runtimes/openai/generated/v2/FsGetMetadataResponse.ts b/src/runtimes/openai/generated/v2/FsGetMetadataResponse.ts deleted file mode 100644 index 14352b9..0000000 --- a/src/runtimes/openai/generated/v2/FsGetMetadataResponse.ts +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Metadata returned by `fs/getMetadata`. - */ -export type FsGetMetadataResponse = { - /** - * Whether the path resolves to a directory. - */ - isDirectory: boolean; - /** - * Whether the path resolves to a regular file. - */ - isFile: boolean; - /** - * Whether the path itself is a symbolic link. - */ - isSymlink: boolean; - /** - * File creation time in Unix milliseconds when available, otherwise `0`. - */ - createdAtMs: number; - /** - * File modification time in Unix milliseconds when available, otherwise `0`. - */ - modifiedAtMs: number; -}; diff --git a/src/runtimes/openai/generated/v2/FsReadDirectoryEntry.ts b/src/runtimes/openai/generated/v2/FsReadDirectoryEntry.ts deleted file mode 100644 index 0d44a75..0000000 --- a/src/runtimes/openai/generated/v2/FsReadDirectoryEntry.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * A directory entry returned by `fs/readDirectory`. - */ -export type FsReadDirectoryEntry = { - /** - * Direct child entry name only, not an absolute or relative path. - */ - fileName: string; - /** - * Whether this entry resolves to a directory. - */ - isDirectory: boolean; - /** - * Whether this entry resolves to a regular file. - */ - isFile: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/FsReadDirectoryParams.ts b/src/runtimes/openai/generated/v2/FsReadDirectoryParams.ts deleted file mode 100644 index 08310e6..0000000 --- a/src/runtimes/openai/generated/v2/FsReadDirectoryParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * List direct child names for a directory. - */ -export type FsReadDirectoryParams = { - /** - * Absolute directory path to read. - */ - path: AbsolutePathBuf; -}; diff --git a/src/runtimes/openai/generated/v2/FsReadDirectoryResponse.ts b/src/runtimes/openai/generated/v2/FsReadDirectoryResponse.ts deleted file mode 100644 index 49e9680..0000000 --- a/src/runtimes/openai/generated/v2/FsReadDirectoryResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FsReadDirectoryEntry } from "./FsReadDirectoryEntry"; - -/** - * Directory entries returned by `fs/readDirectory`. - */ -export type FsReadDirectoryResponse = { - /** - * Direct child entries in the requested directory. - */ - entries: Array; -}; diff --git a/src/runtimes/openai/generated/v2/FsReadFileParams.ts b/src/runtimes/openai/generated/v2/FsReadFileParams.ts deleted file mode 100644 index cb71e2b..0000000 --- a/src/runtimes/openai/generated/v2/FsReadFileParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * Read a file from the host filesystem. - */ -export type FsReadFileParams = { - /** - * Absolute path to read. - */ - path: AbsolutePathBuf; -}; diff --git a/src/runtimes/openai/generated/v2/FsReadFileResponse.ts b/src/runtimes/openai/generated/v2/FsReadFileResponse.ts deleted file mode 100644 index 15906af..0000000 --- a/src/runtimes/openai/generated/v2/FsReadFileResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Base64-encoded file contents returned by `fs/readFile`. - */ -export type FsReadFileResponse = { - /** - * File contents encoded as base64. - */ - dataBase64: string; -}; diff --git a/src/runtimes/openai/generated/v2/FsRemoveParams.ts b/src/runtimes/openai/generated/v2/FsRemoveParams.ts deleted file mode 100644 index dc3948b..0000000 --- a/src/runtimes/openai/generated/v2/FsRemoveParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * Remove a file or directory tree from the host filesystem. - */ -export type FsRemoveParams = { - /** - * Absolute path to remove. - */ - path: AbsolutePathBuf; - /** - * Whether directory removal should recurse. Defaults to `true`. - */ - recursive?: boolean | null; - /** - * Whether missing paths should be ignored. Defaults to `true`. - */ - force?: boolean | null; -}; diff --git a/src/runtimes/openai/generated/v2/FsRemoveResponse.ts b/src/runtimes/openai/generated/v2/FsRemoveResponse.ts deleted file mode 100644 index 981c28f..0000000 --- a/src/runtimes/openai/generated/v2/FsRemoveResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/remove`. - */ -export type FsRemoveResponse = Record; diff --git a/src/runtimes/openai/generated/v2/FsUnwatchParams.ts b/src/runtimes/openai/generated/v2/FsUnwatchParams.ts deleted file mode 100644 index d3f6198..0000000 --- a/src/runtimes/openai/generated/v2/FsUnwatchParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Stop filesystem watch notifications for a prior `fs/watch`. - */ -export type FsUnwatchParams = { - /** - * Watch identifier previously provided to `fs/watch`. - */ - watchId: string; -}; diff --git a/src/runtimes/openai/generated/v2/FsUnwatchResponse.ts b/src/runtimes/openai/generated/v2/FsUnwatchResponse.ts deleted file mode 100644 index 02507d2..0000000 --- a/src/runtimes/openai/generated/v2/FsUnwatchResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/unwatch`. - */ -export type FsUnwatchResponse = Record; diff --git a/src/runtimes/openai/generated/v2/FsWatchParams.ts b/src/runtimes/openai/generated/v2/FsWatchParams.ts deleted file mode 100644 index 342815e..0000000 --- a/src/runtimes/openai/generated/v2/FsWatchParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * Start filesystem watch notifications for an absolute path. - */ -export type FsWatchParams = { - /** - * Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`. - */ - watchId: string; - /** - * Absolute file or directory path to watch. - */ - path: AbsolutePathBuf; -}; diff --git a/src/runtimes/openai/generated/v2/FsWatchResponse.ts b/src/runtimes/openai/generated/v2/FsWatchResponse.ts deleted file mode 100644 index 7b3459f..0000000 --- a/src/runtimes/openai/generated/v2/FsWatchResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * Successful response for `fs/watch`. - */ -export type FsWatchResponse = { - /** - * Canonicalized path associated with the watch. - */ - path: AbsolutePathBuf; -}; diff --git a/src/runtimes/openai/generated/v2/FsWriteFileParams.ts b/src/runtimes/openai/generated/v2/FsWriteFileParams.ts deleted file mode 100644 index c10e70e..0000000 --- a/src/runtimes/openai/generated/v2/FsWriteFileParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -/** - * Write a file on the host filesystem. - */ -export type FsWriteFileParams = { - /** - * Absolute path to write. - */ - path: AbsolutePathBuf; - /** - * File contents encoded as base64. - */ - dataBase64: string; -}; diff --git a/src/runtimes/openai/generated/v2/FsWriteFileResponse.ts b/src/runtimes/openai/generated/v2/FsWriteFileResponse.ts deleted file mode 100644 index ad0ce28..0000000 --- a/src/runtimes/openai/generated/v2/FsWriteFileResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/writeFile`. - */ -export type FsWriteFileResponse = Record; diff --git a/src/runtimes/openai/generated/v2/GetAccountParams.ts b/src/runtimes/openai/generated/v2/GetAccountParams.ts deleted file mode 100644 index 7d8f1d6..0000000 --- a/src/runtimes/openai/generated/v2/GetAccountParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GetAccountParams = { - /** - * When `true`, requests a proactive token refresh before returning. - * - * In managed auth mode this triggers the normal refresh-token flow. In - * external auth mode this flag is ignored. Clients should refresh tokens - * themselves and call `account/login/start` with `chatgptAuthTokens`. - */ - refreshToken?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/GetAccountRateLimitsResponse.ts b/src/runtimes/openai/generated/v2/GetAccountRateLimitsResponse.ts deleted file mode 100644 index 136e934..0000000 --- a/src/runtimes/openai/generated/v2/GetAccountRateLimitsResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RateLimitResetCreditsSummary } from "./RateLimitResetCreditsSummary"; -import type { RateLimitSnapshot } from "./RateLimitSnapshot"; - -export type GetAccountRateLimitsResponse = { - /** - * Backward-compatible single-bucket view; mirrors the historical payload. - */ - rateLimits: RateLimitSnapshot; - /** - * Multi-bucket view keyed by metered `limit_id` (for example, `codex`). - */ - rateLimitsByLimitId: { [key in string]?: RateLimitSnapshot } | null; - rateLimitResetCredits: RateLimitResetCreditsSummary | null; -}; diff --git a/src/runtimes/openai/generated/v2/GetAccountResponse.ts b/src/runtimes/openai/generated/v2/GetAccountResponse.ts deleted file mode 100644 index 75f09dd..0000000 --- a/src/runtimes/openai/generated/v2/GetAccountResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Account } from "./Account"; - -export type GetAccountResponse = { account: Account | null; requiresOpenaiAuth: boolean }; diff --git a/src/runtimes/openai/generated/v2/GetAccountTokenUsageParams.ts b/src/runtimes/openai/generated/v2/GetAccountTokenUsageParams.ts deleted file mode 100644 index 59709e9..0000000 --- a/src/runtimes/openai/generated/v2/GetAccountTokenUsageParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GetAccountTokenUsageParams = { - /** - * When present, read estimated usage for this thread instead of account-wide token activity. - */ - threadId?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/GetAccountTokenUsageResponse.ts b/src/runtimes/openai/generated/v2/GetAccountTokenUsageResponse.ts deleted file mode 100644 index 88f8725..0000000 --- a/src/runtimes/openai/generated/v2/GetAccountTokenUsageResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket"; -import type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary"; -import type { ThreadUsage } from "./ThreadUsage"; - -export type GetAccountTokenUsageResponse = { - summary: AccountTokenUsageSummary; - dailyUsageBuckets: Array | null; - /** - * Estimated usage when a thread was requested and its billing route is available. - */ - threadUsage?: ThreadUsage | null; -}; diff --git a/src/runtimes/openai/generated/v2/GetWorkspaceMessagesResponse.ts b/src/runtimes/openai/generated/v2/GetWorkspaceMessagesResponse.ts deleted file mode 100644 index 8f1c537..0000000 --- a/src/runtimes/openai/generated/v2/GetWorkspaceMessagesResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WorkspaceMessage } from "./WorkspaceMessage"; - -export type GetWorkspaceMessagesResponse = { - /** - * Whether the workspace-message backend route is available for this client. - */ - featureEnabled: boolean; - /** - * Active workspace messages returned by the backend. - */ - messages: Array; -}; diff --git a/src/runtimes/openai/generated/v2/GitInfo.ts b/src/runtimes/openai/generated/v2/GitInfo.ts deleted file mode 100644 index ea62ce2..0000000 --- a/src/runtimes/openai/generated/v2/GitInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GitInfo = { sha: string | null; branch: string | null; originUrl: string | null }; diff --git a/src/runtimes/openai/generated/v2/GrantedPermissionProfile.ts b/src/runtimes/openai/generated/v2/GrantedPermissionProfile.ts deleted file mode 100644 index 47b41a1..0000000 --- a/src/runtimes/openai/generated/v2/GrantedPermissionProfile.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; -import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; - -export type GrantedPermissionProfile = { - network?: AdditionalNetworkPermissions; - fileSystem?: AdditionalFileSystemPermissions; -}; diff --git a/src/runtimes/openai/generated/v2/GuardianApprovalReview.ts b/src/runtimes/openai/generated/v2/GuardianApprovalReview.ts deleted file mode 100644 index 1b466a5..0000000 --- a/src/runtimes/openai/generated/v2/GuardianApprovalReview.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GuardianApprovalReviewStatus } from "./GuardianApprovalReviewStatus"; -import type { GuardianRiskLevel } from "./GuardianRiskLevel"; -import type { GuardianUserAuthorization } from "./GuardianUserAuthorization"; - -/** - * [UNSTABLE] Temporary approval auto-review payload used by - * `item/autoApprovalReview/*` notifications. This shape is expected to change - * soon. - */ -export type GuardianApprovalReview = { - status: GuardianApprovalReviewStatus; - riskLevel: GuardianRiskLevel | null; - userAuthorization: GuardianUserAuthorization | null; - rationale: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/GuardianApprovalReviewAction.ts b/src/runtimes/openai/generated/v2/GuardianApprovalReviewAction.ts deleted file mode 100644 index e7e40d3..0000000 --- a/src/runtimes/openai/generated/v2/GuardianApprovalReviewAction.ts +++ /dev/null @@ -1,42 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { LegacyAppPathString } from "../LegacyAppPathString"; -import type { GuardianCommandSource } from "./GuardianCommandSource"; -import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; -import type { RequestPermissionProfile } from "./RequestPermissionProfile"; - -export type GuardianApprovalReviewAction = - | { type: "command"; source: GuardianCommandSource; command: string; cwd: AbsolutePathBuf } - | { - type: "execve"; - source: GuardianCommandSource; - program: string; - argv: Array; - cwd: AbsolutePathBuf; - } - | { - type: "writeStdin"; - approvalId: string; - processId: string; - stdin: string; - cwd: LegacyAppPathString; - } - | { type: "applyPatch"; cwd: AbsolutePathBuf; files: Array } - | { - type: "networkAccess"; - target: string; - host: string; - protocol: NetworkApprovalProtocol; - port: number; - } - | { - type: "mcpToolCall"; - server: string; - toolName: string; - connectorId: string | null; - connectorName: string | null; - toolTitle: string | null; - } - | { type: "requestPermissions"; reason: string | null; permissions: RequestPermissionProfile }; diff --git a/src/runtimes/openai/generated/v2/GuardianApprovalReviewStatus.ts b/src/runtimes/openai/generated/v2/GuardianApprovalReviewStatus.ts deleted file mode 100644 index 7e84c40..0000000 --- a/src/runtimes/openai/generated/v2/GuardianApprovalReviewStatus.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * [UNSTABLE] Lifecycle state for an approval auto-review. - */ -export type GuardianApprovalReviewStatus = - | "inProgress" - | "approved" - | "denied" - | "timedOut" - | "aborted"; diff --git a/src/runtimes/openai/generated/v2/GuardianCommandSource.ts b/src/runtimes/openai/generated/v2/GuardianCommandSource.ts deleted file mode 100644 index b48e9b0..0000000 --- a/src/runtimes/openai/generated/v2/GuardianCommandSource.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GuardianCommandSource = "shell" | "unifiedExec"; diff --git a/src/runtimes/openai/generated/v2/GuardianRiskLevel.ts b/src/runtimes/openai/generated/v2/GuardianRiskLevel.ts deleted file mode 100644 index 7734016..0000000 --- a/src/runtimes/openai/generated/v2/GuardianRiskLevel.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * [UNSTABLE] Risk level assigned by approval auto-review. - */ -export type GuardianRiskLevel = "low" | "medium" | "high" | "critical"; diff --git a/src/runtimes/openai/generated/v2/GuardianUserAuthorization.ts b/src/runtimes/openai/generated/v2/GuardianUserAuthorization.ts deleted file mode 100644 index 936611f..0000000 --- a/src/runtimes/openai/generated/v2/GuardianUserAuthorization.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * [UNSTABLE] Authorization level assigned by approval auto-review. - */ -export type GuardianUserAuthorization = "unknown" | "low" | "medium" | "high"; diff --git a/src/runtimes/openai/generated/v2/GuardianWarningNotification.ts b/src/runtimes/openai/generated/v2/GuardianWarningNotification.ts deleted file mode 100644 index 763a0f6..0000000 --- a/src/runtimes/openai/generated/v2/GuardianWarningNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GuardianWarningNotification = { - /** - * Thread target for the guardian warning. - */ - threadId: string; - /** - * Concise guardian warning message for the user. - */ - message: string; -}; diff --git a/src/runtimes/openai/generated/v2/HookCompletedNotification.ts b/src/runtimes/openai/generated/v2/HookCompletedNotification.ts deleted file mode 100644 index 49bffae..0000000 --- a/src/runtimes/openai/generated/v2/HookCompletedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HookRunSummary } from "./HookRunSummary"; - -export type HookCompletedNotification = { - threadId: string; - turnId: string | null; - run: HookRunSummary; -}; diff --git a/src/runtimes/openai/generated/v2/HookErrorInfo.ts b/src/runtimes/openai/generated/v2/HookErrorInfo.ts deleted file mode 100644 index 9d66073..0000000 --- a/src/runtimes/openai/generated/v2/HookErrorInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookErrorInfo = { path: string; message: string }; diff --git a/src/runtimes/openai/generated/v2/HookEventName.ts b/src/runtimes/openai/generated/v2/HookEventName.ts deleted file mode 100644 index fe92ff5..0000000 --- a/src/runtimes/openai/generated/v2/HookEventName.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookEventName = - | "preToolUse" - | "permissionRequest" - | "postToolUse" - | "preCompact" - | "postCompact" - | "sessionStart" - | "sessionEnd" - | "userPromptSubmit" - | "subagentStart" - | "subagentStop" - | "stop" - | "interrupt"; diff --git a/src/runtimes/openai/generated/v2/HookExecutionMode.ts b/src/runtimes/openai/generated/v2/HookExecutionMode.ts deleted file mode 100644 index 61f9856..0000000 --- a/src/runtimes/openai/generated/v2/HookExecutionMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookExecutionMode = "sync" | "async"; diff --git a/src/runtimes/openai/generated/v2/HookHandlerType.ts b/src/runtimes/openai/generated/v2/HookHandlerType.ts deleted file mode 100644 index 7dd671e..0000000 --- a/src/runtimes/openai/generated/v2/HookHandlerType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookHandlerType = "command" | "mcpTool" | "prompt" | "agent"; diff --git a/src/runtimes/openai/generated/v2/HookMetadata.ts b/src/runtimes/openai/generated/v2/HookMetadata.ts deleted file mode 100644 index b7f40b5..0000000 --- a/src/runtimes/openai/generated/v2/HookMetadata.ts +++ /dev/null @@ -1,33 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { HookEventName } from "./HookEventName"; -import type { HookSource } from "./HookSource"; -import type { HookTrustStatus } from "./HookTrustStatus"; - -export type HookMetadata = { - key: string; - eventName: HookEventName; - matcher: string | null; - timeoutSec: bigint; - statusMessage: string | null; - /** - * Configured `additionalContext` spill threshold. - * `null` uses 2,500 tokens; `0` disables spilling. - */ - additionalContextLimit: number | null; - sourcePath: AbsolutePathBuf; - source: HookSource; - pluginId: string | null; - displayOrder: bigint; - enabled: boolean; - isManaged: boolean; - currentHash: string; - trustStatus: HookTrustStatus; -} & ( - | { handlerType: "command"; command: string; async: boolean } - | { handlerType: "mcpTool"; server: string; tool: string } - | { handlerType: "prompt" } - | { handlerType: "agent" } -); diff --git a/src/runtimes/openai/generated/v2/HookMigration.ts b/src/runtimes/openai/generated/v2/HookMigration.ts deleted file mode 100644 index 4f4b3f3..0000000 --- a/src/runtimes/openai/generated/v2/HookMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/HookOutputEntry.ts b/src/runtimes/openai/generated/v2/HookOutputEntry.ts deleted file mode 100644 index 9f02b87..0000000 --- a/src/runtimes/openai/generated/v2/HookOutputEntry.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HookOutputEntryKind } from "./HookOutputEntryKind"; - -export type HookOutputEntry = { kind: HookOutputEntryKind; text: string }; diff --git a/src/runtimes/openai/generated/v2/HookOutputEntryKind.ts b/src/runtimes/openai/generated/v2/HookOutputEntryKind.ts deleted file mode 100644 index 090dfe3..0000000 --- a/src/runtimes/openai/generated/v2/HookOutputEntryKind.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookOutputEntryKind = "warning" | "stop" | "feedback" | "context" | "error"; diff --git a/src/runtimes/openai/generated/v2/HookPromptFragment.ts b/src/runtimes/openai/generated/v2/HookPromptFragment.ts deleted file mode 100644 index 74f2455..0000000 --- a/src/runtimes/openai/generated/v2/HookPromptFragment.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookPromptFragment = { text: string; hookRunId: string }; diff --git a/src/runtimes/openai/generated/v2/HookRunStatus.ts b/src/runtimes/openai/generated/v2/HookRunStatus.ts deleted file mode 100644 index ffca7e0..0000000 --- a/src/runtimes/openai/generated/v2/HookRunStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookRunStatus = "running" | "completed" | "failed" | "blocked" | "stopped"; diff --git a/src/runtimes/openai/generated/v2/HookRunSummary.ts b/src/runtimes/openai/generated/v2/HookRunSummary.ts deleted file mode 100644 index 2c2fc5d..0000000 --- a/src/runtimes/openai/generated/v2/HookRunSummary.ts +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { HookEventName } from "./HookEventName"; -import type { HookExecutionMode } from "./HookExecutionMode"; -import type { HookHandlerType } from "./HookHandlerType"; -import type { HookOutputEntry } from "./HookOutputEntry"; -import type { HookRunStatus } from "./HookRunStatus"; -import type { HookScope } from "./HookScope"; -import type { HookSource } from "./HookSource"; - -export type HookRunSummary = { - id: string; - eventName: HookEventName; - handlerType: HookHandlerType; - executionMode: HookExecutionMode; - scope: HookScope; - sourcePath: AbsolutePathBuf; - source: HookSource; - displayOrder: bigint; - status: HookRunStatus; - statusMessage: string | null; - startedAt: bigint; - completedAt: bigint | null; - durationMs: bigint | null; - entries: Array; -}; diff --git a/src/runtimes/openai/generated/v2/HookScope.ts b/src/runtimes/openai/generated/v2/HookScope.ts deleted file mode 100644 index ff6f8bf..0000000 --- a/src/runtimes/openai/generated/v2/HookScope.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookScope = "thread" | "turn"; diff --git a/src/runtimes/openai/generated/v2/HookSource.ts b/src/runtimes/openai/generated/v2/HookSource.ts deleted file mode 100644 index e9e4cac..0000000 --- a/src/runtimes/openai/generated/v2/HookSource.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookSource = - | "system" - | "user" - | "project" - | "mdm" - | "sessionFlags" - | "plugin" - | "cloudRequirements" - | "cloudManagedConfig" - | "legacyManagedConfigFile" - | "legacyManagedConfigMdm" - | "unknown"; diff --git a/src/runtimes/openai/generated/v2/HookStartedNotification.ts b/src/runtimes/openai/generated/v2/HookStartedNotification.ts deleted file mode 100644 index 880bb40..0000000 --- a/src/runtimes/openai/generated/v2/HookStartedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HookRunSummary } from "./HookRunSummary"; - -export type HookStartedNotification = { - threadId: string; - turnId: string | null; - run: HookRunSummary; -}; diff --git a/src/runtimes/openai/generated/v2/HookTrustStatus.ts b/src/runtimes/openai/generated/v2/HookTrustStatus.ts deleted file mode 100644 index 692fdc4..0000000 --- a/src/runtimes/openai/generated/v2/HookTrustStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookTrustStatus = "managed" | "untrusted" | "trusted" | "modified"; diff --git a/src/runtimes/openai/generated/v2/HooksListEntry.ts b/src/runtimes/openai/generated/v2/HooksListEntry.ts deleted file mode 100644 index 354ead3..0000000 --- a/src/runtimes/openai/generated/v2/HooksListEntry.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HookErrorInfo } from "./HookErrorInfo"; -import type { HookMetadata } from "./HookMetadata"; - -export type HooksListEntry = { - cwd: string; - hooks: Array; - warnings: Array; - errors: Array; -}; diff --git a/src/runtimes/openai/generated/v2/HooksListParams.ts b/src/runtimes/openai/generated/v2/HooksListParams.ts deleted file mode 100644 index 3f85dcb..0000000 --- a/src/runtimes/openai/generated/v2/HooksListParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HooksListParams = { - /** - * When empty, defaults to the current session working directory. - */ - cwds?: Array; -}; diff --git a/src/runtimes/openai/generated/v2/HooksListResponse.ts b/src/runtimes/openai/generated/v2/HooksListResponse.ts deleted file mode 100644 index 44f439c..0000000 --- a/src/runtimes/openai/generated/v2/HooksListResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HooksListEntry } from "./HooksListEntry"; - -export type HooksListResponse = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/InAppBrowserRequirements.ts b/src/runtimes/openai/generated/v2/InAppBrowserRequirements.ts deleted file mode 100644 index 45ab92d..0000000 --- a/src/runtimes/openai/generated/v2/InAppBrowserRequirements.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type InAppBrowserRequirements = { allowExternalBrowserSettingsImport: boolean | null }; diff --git a/src/runtimes/openai/generated/v2/InstalledApp.ts b/src/runtimes/openai/generated/v2/InstalledApp.ts deleted file mode 100644 index 9ddd088..0000000 --- a/src/runtimes/openai/generated/v2/InstalledApp.ts +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Installed connector runtime state. - */ -export type InstalledApp = { - id: string; - /** - * Best-effort name carried by the runtime tool catalog. Canonical app metadata remains owned - * by `app/read`. - */ - runtimeName: string | null; - /** - * Effective enabled state after applying global, workspace, local, and managed configuration - * at read time. - */ - enabled: boolean; - /** - * Whether the connector is enabled and has a non-synthetic, model-visible tool allowed by - * effective MCP and app/tool policy in the committed runtime snapshot. - */ - callable: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ItemCompletedNotification.ts b/src/runtimes/openai/generated/v2/ItemCompletedNotification.ts deleted file mode 100644 index 18220a1..0000000 --- a/src/runtimes/openai/generated/v2/ItemCompletedNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadItem } from "./ThreadItem"; - -export type ItemCompletedNotification = { - item: ThreadItem; - threadId: string; - turnId: string; - /** - * Unix timestamp (in milliseconds) when this item lifecycle completed. - */ - completedAtMs: number; -}; diff --git a/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewCompletedNotification.ts b/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewCompletedNotification.ts deleted file mode 100644 index 6d62f0a..0000000 --- a/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewCompletedNotification.ts +++ /dev/null @@ -1,46 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource"; -import type { GuardianApprovalReview } from "./GuardianApprovalReview"; -import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction"; - -/** - * [UNSTABLE] Temporary notification payload for approval auto-review. This - * shape is expected to change soon. - */ -export type ItemGuardianApprovalReviewCompletedNotification = { - threadId: string; - turnId: string; - /** - * Unix timestamp (in milliseconds) when this review started. - */ - startedAtMs: number; - /** - * Unix timestamp (in milliseconds) when this review completed. - */ - completedAtMs: number; - /** - * Stable identifier for this review. - */ - reviewId: string; - /** - * Identifier for the reviewed item or tool call when one exists. - * - * In most cases, one review maps to one target item. The exceptions are - * - execve reviews, where a single command may contain multiple execve - * calls to review (only possible when using the shell_zsh_fork feature) - * - stdin reviews, which refer to the existing parent command item and - * have a separate approval ID in the action payload - * - network policy reviews, where there is no target item - * - * A network call is triggered by a CommandExecution item, so having a - * target_item_id set to the CommandExecution item would be misleading - * because the review is about the network call, not the command execution. - * Therefore, target_item_id is set to None for network policy reviews. - */ - targetItemId: string | null; - decisionSource: AutoReviewDecisionSource; - review: GuardianApprovalReview; - action: GuardianApprovalReviewAction; -}; diff --git a/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewStartedNotification.ts b/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewStartedNotification.ts deleted file mode 100644 index 86e9a61..0000000 --- a/src/runtimes/openai/generated/v2/ItemGuardianApprovalReviewStartedNotification.ts +++ /dev/null @@ -1,40 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GuardianApprovalReview } from "./GuardianApprovalReview"; -import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction"; - -/** - * [UNSTABLE] Temporary notification payload for approval auto-review. This - * shape is expected to change soon. - */ -export type ItemGuardianApprovalReviewStartedNotification = { - threadId: string; - turnId: string; - /** - * Unix timestamp (in milliseconds) when this review started. - */ - startedAtMs: number; - /** - * Stable identifier for this review. - */ - reviewId: string; - /** - * Identifier for the reviewed item or tool call when one exists. - * - * In most cases, one review maps to one target item. The exceptions are - * - execve reviews, where a single command may contain multiple execve - * calls to review (only possible when using the shell_zsh_fork feature) - * - stdin reviews, which refer to the existing parent command item and - * have a separate approval ID in the action payload - * - network policy reviews, where there is no target item - * - * A network call is triggered by a CommandExecution item, so having a - * target_item_id set to the CommandExecution item would be misleading - * because the review is about the network call, not the command execution. - * Therefore, target_item_id is set to None for network policy reviews. - */ - targetItemId: string | null; - review: GuardianApprovalReview; - action: GuardianApprovalReviewAction; -}; diff --git a/src/runtimes/openai/generated/v2/ItemStartedNotification.ts b/src/runtimes/openai/generated/v2/ItemStartedNotification.ts deleted file mode 100644 index e7bbec6..0000000 --- a/src/runtimes/openai/generated/v2/ItemStartedNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadItem } from "./ThreadItem"; - -export type ItemStartedNotification = { - item: ThreadItem; - threadId: string; - turnId: string; - /** - * Unix timestamp (in milliseconds) when this item lifecycle started. - */ - startedAtMs: number; -}; diff --git a/src/runtimes/openai/generated/v2/ListMcpServerStatusParams.ts b/src/runtimes/openai/generated/v2/ListMcpServerStatusParams.ts deleted file mode 100644 index 655eb3b..0000000 --- a/src/runtimes/openai/generated/v2/ListMcpServerStatusParams.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpServerStatusDetail } from "./McpServerStatusDetail"; - -export type ListMcpServerStatusParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a server-defined value. - */ - limit?: number | null; - /** - * Controls how much MCP inventory data to fetch for each server. - * Defaults to `Full` when omitted. - */ - detail?: McpServerStatusDetail | null; - threadId?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ListMcpServerStatusResponse.ts b/src/runtimes/openai/generated/v2/ListMcpServerStatusResponse.ts deleted file mode 100644 index 9e08e03..0000000 --- a/src/runtimes/openai/generated/v2/ListMcpServerStatusResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpServerStatus } from "./McpServerStatus"; - -export type ListMcpServerStatusResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/LoginAccountParams.ts b/src/runtimes/openai/generated/v2/LoginAccountParams.ts deleted file mode 100644 index fd5964e..0000000 --- a/src/runtimes/openai/generated/v2/LoginAccountParams.ts +++ /dev/null @@ -1,41 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LoginAppBrand } from "./LoginAppBrand"; - -export type LoginAccountParams = - | { type: "apiKey"; apiKey: string } - | { - type: "chatgpt"; - codexStreamlinedLogin?: boolean; - useHostedLoginSuccessPage?: boolean; - appBrand?: LoginAppBrand | null; - } - | { type: "chatgptDeviceCode" } - | { - type: "chatgptAuthTokens"; - /** - * Access token (JWT) supplied by the client. - * This token is used for backend API requests and email extraction. - */ - accessToken: string; - /** - * Workspace/account identifier supplied by the client. - */ - chatgptAccountId: string; - /** - * Optional plan type supplied by the client. - * - * When `null`, Codex attempts to derive the plan type from access-token - * claims. If unavailable, the plan defaults to `unknown`. - */ - chatgptPlanType?: string | null; - } - | { type: "amazonBedrock"; apiKey: string; region: string } - | { - type: "amazonBedrockAccessKeys"; - accessKeyId: string; - secretAccessKey: string; - sessionToken?: string | null; - region: string; - }; diff --git a/src/runtimes/openai/generated/v2/LoginAccountResponse.ts b/src/runtimes/openai/generated/v2/LoginAccountResponse.ts deleted file mode 100644 index 5d195a6..0000000 --- a/src/runtimes/openai/generated/v2/LoginAccountResponse.ts +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type LoginAccountResponse = - | { type: "apiKey" } - | { - type: "chatgpt"; - loginId: string; - /** - * URL the client should open in a browser to initiate the OAuth flow. - */ - authUrl: string; - } - | { - type: "chatgptDeviceCode"; - loginId: string; - /** - * URL the client should open in a browser to complete device code authorization. - */ - verificationUrl: string; - /** - * One-time code the user must enter after signing in. - */ - userCode: string; - } - | { type: "chatgptAuthTokens" } - | { type: "amazonBedrock" }; diff --git a/src/runtimes/openai/generated/v2/LoginAppBrand.ts b/src/runtimes/openai/generated/v2/LoginAppBrand.ts deleted file mode 100644 index c06d12f..0000000 --- a/src/runtimes/openai/generated/v2/LoginAppBrand.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type LoginAppBrand = "codex" | "chatgpt"; diff --git a/src/runtimes/openai/generated/v2/LogoutAccountResponse.ts b/src/runtimes/openai/generated/v2/LogoutAccountResponse.ts deleted file mode 100644 index ec85cf0..0000000 --- a/src/runtimes/openai/generated/v2/LogoutAccountResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type LogoutAccountResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ManagedHooksRequirements.ts b/src/runtimes/openai/generated/v2/ManagedHooksRequirements.ts deleted file mode 100644 index 431afdf..0000000 --- a/src/runtimes/openai/generated/v2/ManagedHooksRequirements.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup"; - -export type ManagedHooksRequirements = { - managedDir: string | null; - windowsManagedDir: string | null; - PreToolUse: Array; - PermissionRequest: Array; - PostToolUse: Array; - PreCompact: Array; - PostCompact: Array; - SessionStart: Array; - SessionEnd: Array; - UserPromptSubmit: Array; - SubagentStart: Array; - SubagentStop: Array; - Stop: Array; - Interrupt: Array; -}; diff --git a/src/runtimes/openai/generated/v2/MarketplaceAddParams.ts b/src/runtimes/openai/generated/v2/MarketplaceAddParams.ts deleted file mode 100644 index 40f5378..0000000 --- a/src/runtimes/openai/generated/v2/MarketplaceAddParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceAddParams = { - source: string; - refName?: string | null; - sparsePaths?: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/MarketplaceAddResponse.ts b/src/runtimes/openai/generated/v2/MarketplaceAddResponse.ts deleted file mode 100644 index 96ca042..0000000 --- a/src/runtimes/openai/generated/v2/MarketplaceAddResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type MarketplaceAddResponse = { - marketplaceName: string; - installedRoot: AbsolutePathBuf; - alreadyAdded: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/MarketplaceInterface.ts b/src/runtimes/openai/generated/v2/MarketplaceInterface.ts deleted file mode 100644 index 71f55eb..0000000 --- a/src/runtimes/openai/generated/v2/MarketplaceInterface.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceInterface = { displayName: string | null }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceLoadErrorInfo.ts b/src/runtimes/openai/generated/v2/MarketplaceLoadErrorInfo.ts deleted file mode 100644 index 15debd8..0000000 --- a/src/runtimes/openai/generated/v2/MarketplaceLoadErrorInfo.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type MarketplaceLoadErrorInfo = { marketplacePath: AbsolutePathBuf; message: string }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceRemoveParams.ts b/src/runtimes/openai/generated/v2/MarketplaceRemoveParams.ts deleted file mode 100644 index 88e6f3f..0000000 --- a/src/runtimes/openai/generated/v2/MarketplaceRemoveParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceRemoveParams = { marketplaceName: string }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceRemoveResponse.ts b/src/runtimes/openai/generated/v2/MarketplaceRemoveResponse.ts deleted file mode 100644 index 4ff8926..0000000 --- a/src/runtimes/openai/generated/v2/MarketplaceRemoveResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type MarketplaceRemoveResponse = { - marketplaceName: string; - installedRoot: AbsolutePathBuf | null; -}; diff --git a/src/runtimes/openai/generated/v2/MarketplaceUpgradeErrorInfo.ts b/src/runtimes/openai/generated/v2/MarketplaceUpgradeErrorInfo.ts deleted file mode 100644 index 6f3dfc6..0000000 --- a/src/runtimes/openai/generated/v2/MarketplaceUpgradeErrorInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceUpgradeErrorInfo = { marketplaceName: string; message: string }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceUpgradeParams.ts b/src/runtimes/openai/generated/v2/MarketplaceUpgradeParams.ts deleted file mode 100644 index 2061385..0000000 --- a/src/runtimes/openai/generated/v2/MarketplaceUpgradeParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceUpgradeParams = { marketplaceName?: string | null }; diff --git a/src/runtimes/openai/generated/v2/MarketplaceUpgradeResponse.ts b/src/runtimes/openai/generated/v2/MarketplaceUpgradeResponse.ts deleted file mode 100644 index 2affee4..0000000 --- a/src/runtimes/openai/generated/v2/MarketplaceUpgradeResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo"; - -export type MarketplaceUpgradeResponse = { - selectedMarketplaces: Array; - upgradedRoots: Array; - errors: Array; -}; diff --git a/src/runtimes/openai/generated/v2/McpAuthStatus.ts b/src/runtimes/openai/generated/v2/McpAuthStatus.ts deleted file mode 100644 index 67d1233..0000000 --- a/src/runtimes/openai/generated/v2/McpAuthStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpAuthStatus = "unknown" | "unsupported" | "notLoggedIn" | "bearerToken" | "oAuth"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationArrayType.ts b/src/runtimes/openai/generated/v2/McpElicitationArrayType.ts deleted file mode 100644 index 066b44e..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationArrayType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationArrayType = "array"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationBooleanSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationBooleanSchema.ts deleted file mode 100644 index 3b77010..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationBooleanSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationBooleanType } from "./McpElicitationBooleanType"; - -export type McpElicitationBooleanSchema = { - type: McpElicitationBooleanType; - title?: string; - description?: string; - default?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationBooleanType.ts b/src/runtimes/openai/generated/v2/McpElicitationBooleanType.ts deleted file mode 100644 index f2b9ed4..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationBooleanType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationBooleanType = "boolean"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationConstOption.ts b/src/runtimes/openai/generated/v2/McpElicitationConstOption.ts deleted file mode 100644 index eed92e2..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationConstOption.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationConstOption = { const: string; title: string }; diff --git a/src/runtimes/openai/generated/v2/McpElicitationEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationEnumSchema.ts deleted file mode 100644 index 2b2f7ba..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationEnumSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationLegacyTitledEnumSchema } from "./McpElicitationLegacyTitledEnumSchema"; -import type { McpElicitationMultiSelectEnumSchema } from "./McpElicitationMultiSelectEnumSchema"; -import type { McpElicitationSingleSelectEnumSchema } from "./McpElicitationSingleSelectEnumSchema"; - -export type McpElicitationEnumSchema = - | McpElicitationSingleSelectEnumSchema - | McpElicitationMultiSelectEnumSchema - | McpElicitationLegacyTitledEnumSchema; diff --git a/src/runtimes/openai/generated/v2/McpElicitationLegacyTitledEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationLegacyTitledEnumSchema.ts deleted file mode 100644 index cb43bcb..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationLegacyTitledEnumSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationStringType } from "./McpElicitationStringType"; - -export type McpElicitationLegacyTitledEnumSchema = { - type: McpElicitationStringType; - title?: string; - description?: string; - enum: Array; - enumNames?: Array; - default?: string; -}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationMultiSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationMultiSelectEnumSchema.ts deleted file mode 100644 index f99a19b..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationMultiSelectEnumSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationTitledMultiSelectEnumSchema } from "./McpElicitationTitledMultiSelectEnumSchema"; -import type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema"; - -export type McpElicitationMultiSelectEnumSchema = - | McpElicitationUntitledMultiSelectEnumSchema - | McpElicitationTitledMultiSelectEnumSchema; diff --git a/src/runtimes/openai/generated/v2/McpElicitationNumberSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationNumberSchema.ts deleted file mode 100644 index 8cd9455..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationNumberSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationNumberType } from "./McpElicitationNumberType"; - -export type McpElicitationNumberSchema = { - type: McpElicitationNumberType; - title?: string; - description?: string; - minimum?: number; - maximum?: number; - default?: number; -}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationNumberType.ts b/src/runtimes/openai/generated/v2/McpElicitationNumberType.ts deleted file mode 100644 index 96a9ded..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationNumberType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationNumberType = "number" | "integer"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationObjectType.ts b/src/runtimes/openai/generated/v2/McpElicitationObjectType.ts deleted file mode 100644 index 2449a0c..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationObjectType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationObjectType = "object"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationPrimitiveSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationPrimitiveSchema.ts deleted file mode 100644 index a8b6668..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationPrimitiveSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema"; -import type { McpElicitationEnumSchema } from "./McpElicitationEnumSchema"; -import type { McpElicitationNumberSchema } from "./McpElicitationNumberSchema"; -import type { McpElicitationStringSchema } from "./McpElicitationStringSchema"; - -export type McpElicitationPrimitiveSchema = - | McpElicitationEnumSchema - | McpElicitationStringSchema - | McpElicitationNumberSchema - | McpElicitationBooleanSchema; diff --git a/src/runtimes/openai/generated/v2/McpElicitationSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationSchema.ts deleted file mode 100644 index 34f703a..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationSchema.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationObjectType } from "./McpElicitationObjectType"; -import type { McpElicitationPrimitiveSchema } from "./McpElicitationPrimitiveSchema"; - -/** - * Typed form schema for MCP `elicitation/create` requests. - * - * This matches the `requestedSchema` shape from the MCP 2025-11-25 - * `ElicitRequestFormParams` schema. - */ -export type McpElicitationSchema = { - $schema?: string; - type: McpElicitationObjectType; - properties: { [key in string]?: McpElicitationPrimitiveSchema }; - required?: Array; -}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationSingleSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationSingleSelectEnumSchema.ts deleted file mode 100644 index 3d1ce01..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationSingleSelectEnumSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitationTitledSingleSelectEnumSchema"; -import type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema"; - -export type McpElicitationSingleSelectEnumSchema = - | McpElicitationUntitledSingleSelectEnumSchema - | McpElicitationTitledSingleSelectEnumSchema; diff --git a/src/runtimes/openai/generated/v2/McpElicitationStringFormat.ts b/src/runtimes/openai/generated/v2/McpElicitationStringFormat.ts deleted file mode 100644 index 9891d4c..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationStringFormat.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationStringFormat = "email" | "uri" | "date" | "date-time"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationStringSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationStringSchema.ts deleted file mode 100644 index 5161f23..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationStringSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationStringFormat } from "./McpElicitationStringFormat"; -import type { McpElicitationStringType } from "./McpElicitationStringType"; - -export type McpElicitationStringSchema = { - type: McpElicitationStringType; - title?: string; - description?: string; - minLength?: number; - maxLength?: number; - format?: McpElicitationStringFormat; - default?: string; -}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationStringType.ts b/src/runtimes/openai/generated/v2/McpElicitationStringType.ts deleted file mode 100644 index bf2ddfa..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationStringType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationStringType = "string"; diff --git a/src/runtimes/openai/generated/v2/McpElicitationTitledEnumItems.ts b/src/runtimes/openai/generated/v2/McpElicitationTitledEnumItems.ts deleted file mode 100644 index 3b6bab9..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationTitledEnumItems.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationConstOption } from "./McpElicitationConstOption"; - -export type McpElicitationTitledEnumItems = { anyOf: Array }; diff --git a/src/runtimes/openai/generated/v2/McpElicitationTitledMultiSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationTitledMultiSelectEnumSchema.ts deleted file mode 100644 index 5128669..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationTitledMultiSelectEnumSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationArrayType } from "./McpElicitationArrayType"; -import type { McpElicitationTitledEnumItems } from "./McpElicitationTitledEnumItems"; - -export type McpElicitationTitledMultiSelectEnumSchema = { - type: McpElicitationArrayType; - title?: string; - description?: string; - minItems?: bigint; - maxItems?: bigint; - items: McpElicitationTitledEnumItems; - default?: Array; -}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationTitledSingleSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationTitledSingleSelectEnumSchema.ts deleted file mode 100644 index af4dcc8..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationTitledSingleSelectEnumSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationConstOption } from "./McpElicitationConstOption"; -import type { McpElicitationStringType } from "./McpElicitationStringType"; - -export type McpElicitationTitledSingleSelectEnumSchema = { - type: McpElicitationStringType; - title?: string; - description?: string; - oneOf: Array; - default?: string; -}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationUntitledEnumItems.ts b/src/runtimes/openai/generated/v2/McpElicitationUntitledEnumItems.ts deleted file mode 100644 index 0e33a9b..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationUntitledEnumItems.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationStringType } from "./McpElicitationStringType"; - -export type McpElicitationUntitledEnumItems = { - type: McpElicitationStringType; - enum: Array; -}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationUntitledMultiSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationUntitledMultiSelectEnumSchema.ts deleted file mode 100644 index b5fd30e..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationUntitledMultiSelectEnumSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationArrayType } from "./McpElicitationArrayType"; -import type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems"; - -export type McpElicitationUntitledMultiSelectEnumSchema = { - type: McpElicitationArrayType; - title?: string; - description?: string; - minItems?: bigint; - maxItems?: bigint; - items: McpElicitationUntitledEnumItems; - default?: Array; -}; diff --git a/src/runtimes/openai/generated/v2/McpElicitationUntitledSingleSelectEnumSchema.ts b/src/runtimes/openai/generated/v2/McpElicitationUntitledSingleSelectEnumSchema.ts deleted file mode 100644 index 65608a9..0000000 --- a/src/runtimes/openai/generated/v2/McpElicitationUntitledSingleSelectEnumSchema.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationStringType } from "./McpElicitationStringType"; - -export type McpElicitationUntitledSingleSelectEnumSchema = { - type: McpElicitationStringType; - title?: string; - description?: string; - enum: Array; - default?: string; -}; diff --git a/src/runtimes/openai/generated/v2/McpResourceReadParams.ts b/src/runtimes/openai/generated/v2/McpResourceReadParams.ts deleted file mode 100644 index b173fb2..0000000 --- a/src/runtimes/openai/generated/v2/McpResourceReadParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpResourceReadParams = { - threadId?: string | null; - /** - * Originating MCP tool call used to select the resource's app. - */ - originCallId?: string | null; - server: string; - uri: string; - connectorId?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/McpResourceReadResponse.ts b/src/runtimes/openai/generated/v2/McpResourceReadResponse.ts deleted file mode 100644 index c65eb79..0000000 --- a/src/runtimes/openai/generated/v2/McpResourceReadResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ResourceContent } from "../ResourceContent"; - -export type McpResourceReadResponse = { - contents: Array; - /** - * Originating call when the server applied app-specific resource scoping. - */ - originCallId: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/McpServerConnectionStatus.ts b/src/runtimes/openai/generated/v2/McpServerConnectionStatus.ts deleted file mode 100644 index df1912a..0000000 --- a/src/runtimes/openai/generated/v2/McpServerConnectionStatus.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerConnectionStatus = - | "notStarted" - | "starting" - | "connected" - | "authenticationRequired" - | "failed" - | "cancelled" - | "disabled"; diff --git a/src/runtimes/openai/generated/v2/McpServerElicitationAction.ts b/src/runtimes/openai/generated/v2/McpServerElicitationAction.ts deleted file mode 100644 index 7be134c..0000000 --- a/src/runtimes/openai/generated/v2/McpServerElicitationAction.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerElicitationAction = "accept" | "decline" | "cancel"; diff --git a/src/runtimes/openai/generated/v2/McpServerElicitationRequestParams.ts b/src/runtimes/openai/generated/v2/McpServerElicitationRequestParams.ts deleted file mode 100644 index 0080563..0000000 --- a/src/runtimes/openai/generated/v2/McpServerElicitationRequestParams.ts +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; -import type { McpElicitationSchema } from "./McpElicitationSchema"; - -export type McpServerElicitationRequestParams = { - threadId: string; - /** - * Active Codex turn when this elicitation was observed, if app-server could correlate one. - * - * This is nullable because MCP models elicitation as a standalone server-to-client request - * identified by the MCP server request id. It may be triggered during a turn, but turn - * context is app-server correlation rather than part of the protocol identity of the - * elicitation itself. - */ - turnId: string | null; - serverName: string; -} & ( - | { - mode: "form"; - _meta: JsonValue | null; - message: string; - requestedSchema: McpElicitationSchema; - } - | { mode: "openai/form"; _meta: JsonValue | null; message: string; requestedSchema: JsonValue } - | { mode: "url"; _meta: JsonValue | null; message: string; url: string; elicitationId: string } -); diff --git a/src/runtimes/openai/generated/v2/McpServerElicitationRequestResponse.ts b/src/runtimes/openai/generated/v2/McpServerElicitationRequestResponse.ts deleted file mode 100644 index e75b7cc..0000000 --- a/src/runtimes/openai/generated/v2/McpServerElicitationRequestResponse.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; -import type { McpServerElicitationAction } from "./McpServerElicitationAction"; - -export type McpServerElicitationRequestResponse = { - action: McpServerElicitationAction; - /** - * Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`. - * - * This is nullable because decline/cancel responses have no content. - */ - content: JsonValue | null; - /** - * Optional client metadata for form-mode action handling. - */ - _meta: JsonValue | null; -}; diff --git a/src/runtimes/openai/generated/v2/McpServerEventNotification.ts b/src/runtimes/openai/generated/v2/McpServerEventNotification.ts deleted file mode 100644 index 5fa1dac..0000000 --- a/src/runtimes/openai/generated/v2/McpServerEventNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type McpServerEventNotification = { method: string; params: JsonValue }; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamNotification.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamNotification.ts deleted file mode 100644 index 188f94f..0000000 --- a/src/runtimes/openai/generated/v2/McpServerEventStreamNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpServerEventNotification } from "./McpServerEventNotification"; - -export type McpServerEventStreamNotification = { - subscriptionId: string; - notification: McpServerEventNotification; -}; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamStartParams.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamStartParams.ts deleted file mode 100644 index 9f213e2..0000000 --- a/src/runtimes/openai/generated/v2/McpServerEventStreamStartParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type McpServerEventStreamStartParams = { - threadId: string; - server: string; - subscriptionId: string; - name: string; - arguments: JsonValue; - _meta?: JsonValue | null; -}; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamStartResponse.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamStartResponse.ts deleted file mode 100644 index 382bc67..0000000 --- a/src/runtimes/openai/generated/v2/McpServerEventStreamStartResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerEventStreamStartResponse = Record; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamStopParams.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamStopParams.ts deleted file mode 100644 index 2a6c063..0000000 --- a/src/runtimes/openai/generated/v2/McpServerEventStreamStopParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerEventStreamStopParams = { subscriptionId: string }; diff --git a/src/runtimes/openai/generated/v2/McpServerEventStreamStopResponse.ts b/src/runtimes/openai/generated/v2/McpServerEventStreamStopResponse.ts deleted file mode 100644 index 9e9c2e9..0000000 --- a/src/runtimes/openai/generated/v2/McpServerEventStreamStopResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerEventStreamStopResponse = Record; diff --git a/src/runtimes/openai/generated/v2/McpServerMigration.ts b/src/runtimes/openai/generated/v2/McpServerMigration.ts deleted file mode 100644 index fe7f200..0000000 --- a/src/runtimes/openai/generated/v2/McpServerMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/McpServerOauthClientRegistration.ts b/src/runtimes/openai/generated/v2/McpServerOauthClientRegistration.ts deleted file mode 100644 index 052150b..0000000 --- a/src/runtimes/openai/generated/v2/McpServerOauthClientRegistration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerOauthClientRegistration = "auto" | "cimd" | "dcr"; diff --git a/src/runtimes/openai/generated/v2/McpServerOauthLoginCompletedNotification.ts b/src/runtimes/openai/generated/v2/McpServerOauthLoginCompletedNotification.ts deleted file mode 100644 index 322e219..0000000 --- a/src/runtimes/openai/generated/v2/McpServerOauthLoginCompletedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerOauthLoginCompletedNotification = { - name: string; - threadId: string | null; - success: boolean; - error?: string; -}; diff --git a/src/runtimes/openai/generated/v2/McpServerOauthLoginParams.ts b/src/runtimes/openai/generated/v2/McpServerOauthLoginParams.ts deleted file mode 100644 index 6e8dc7e..0000000 --- a/src/runtimes/openai/generated/v2/McpServerOauthLoginParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpServerOauthClientRegistration } from "./McpServerOauthClientRegistration"; - -export type McpServerOauthLoginParams = { - name: string; - threadId?: string | null; - /** - * Registration strategy for this login only; omission selects automatic discovery. - */ - clientRegistration?: McpServerOauthClientRegistration | null; - scopes?: Array | null; - timeoutSecs?: bigint | null; -}; diff --git a/src/runtimes/openai/generated/v2/McpServerOauthLoginResponse.ts b/src/runtimes/openai/generated/v2/McpServerOauthLoginResponse.ts deleted file mode 100644 index b119e07..0000000 --- a/src/runtimes/openai/generated/v2/McpServerOauthLoginResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerOauthLoginResponse = { authorizationUrl: string }; diff --git a/src/runtimes/openai/generated/v2/McpServerRefreshResponse.ts b/src/runtimes/openai/generated/v2/McpServerRefreshResponse.ts deleted file mode 100644 index 48a25d2..0000000 --- a/src/runtimes/openai/generated/v2/McpServerRefreshResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerRefreshResponse = Record; diff --git a/src/runtimes/openai/generated/v2/McpServerStartupFailureReason.ts b/src/runtimes/openai/generated/v2/McpServerStartupFailureReason.ts deleted file mode 100644 index 0373e54..0000000 --- a/src/runtimes/openai/generated/v2/McpServerStartupFailureReason.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerStartupFailureReason = "reauthenticationRequired"; diff --git a/src/runtimes/openai/generated/v2/McpServerStartupState.ts b/src/runtimes/openai/generated/v2/McpServerStartupState.ts deleted file mode 100644 index c62babc..0000000 --- a/src/runtimes/openai/generated/v2/McpServerStartupState.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerStartupState = "starting" | "ready" | "failed" | "cancelled"; diff --git a/src/runtimes/openai/generated/v2/McpServerStatus.ts b/src/runtimes/openai/generated/v2/McpServerStatus.ts deleted file mode 100644 index 98a039c..0000000 --- a/src/runtimes/openai/generated/v2/McpServerStatus.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpServerInfo } from "../McpServerInfo"; -import type { Resource } from "../Resource"; -import type { ResourceTemplate } from "../ResourceTemplate"; -import type { Tool } from "../Tool"; -import type { McpAuthStatus } from "./McpAuthStatus"; -import type { McpServerConnectionStatus } from "./McpServerConnectionStatus"; - -export type McpServerStatus = { - name: string; - /** - * Current thread-runtime connection state; null when unavailable or the configuration changed. - */ - runtimeStatus: McpServerConnectionStatus | null; - pluginId: string | null; - serverInfo: McpServerInfo | null; - tools: { [key in string]?: Tool }; - resources: Array; - resourceTemplates: Array; - authStatus: McpAuthStatus; -}; diff --git a/src/runtimes/openai/generated/v2/McpServerStatusDetail.ts b/src/runtimes/openai/generated/v2/McpServerStatusDetail.ts deleted file mode 100644 index ab97cc2..0000000 --- a/src/runtimes/openai/generated/v2/McpServerStatusDetail.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerStatusDetail = "full" | "toolsAndAuthOnly"; diff --git a/src/runtimes/openai/generated/v2/McpServerStatusUpdatedNotification.ts b/src/runtimes/openai/generated/v2/McpServerStatusUpdatedNotification.ts deleted file mode 100644 index b439318..0000000 --- a/src/runtimes/openai/generated/v2/McpServerStatusUpdatedNotification.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpServerStartupFailureReason } from "./McpServerStartupFailureReason"; -import type { McpServerStartupState } from "./McpServerStartupState"; - -export type McpServerStatusUpdatedNotification = { - threadId: string | null; - name: string; - status: McpServerStartupState; - error: string | null; - failureReason: McpServerStartupFailureReason | null; -}; diff --git a/src/runtimes/openai/generated/v2/McpServerToolCallParams.ts b/src/runtimes/openai/generated/v2/McpServerToolCallParams.ts deleted file mode 100644 index 7bff8e2..0000000 --- a/src/runtimes/openai/generated/v2/McpServerToolCallParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type McpServerToolCallParams = { - threadId: string; - server: string; - tool: string; - arguments?: JsonValue; - _meta?: JsonValue; -}; diff --git a/src/runtimes/openai/generated/v2/McpServerToolCallResponse.ts b/src/runtimes/openai/generated/v2/McpServerToolCallResponse.ts deleted file mode 100644 index 75c9de1..0000000 --- a/src/runtimes/openai/generated/v2/McpServerToolCallResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type McpServerToolCallResponse = { - content: Array; - structuredContent?: JsonValue; - isError?: boolean; - _meta?: JsonValue; -}; diff --git a/src/runtimes/openai/generated/v2/McpToolCallAppContext.ts b/src/runtimes/openai/generated/v2/McpToolCallAppContext.ts deleted file mode 100644 index 9101810..0000000 --- a/src/runtimes/openai/generated/v2/McpToolCallAppContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpToolCallAppContext = { - connectorId: string; - linkId: string | null; - resourceUri: string | null; - appName: string | null; - actionName: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/McpToolCallError.ts b/src/runtimes/openai/generated/v2/McpToolCallError.ts deleted file mode 100644 index f11054a..0000000 --- a/src/runtimes/openai/generated/v2/McpToolCallError.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpToolCallError = { message: string }; diff --git a/src/runtimes/openai/generated/v2/McpToolCallProgressNotification.ts b/src/runtimes/openai/generated/v2/McpToolCallProgressNotification.ts deleted file mode 100644 index 3f3137e..0000000 --- a/src/runtimes/openai/generated/v2/McpToolCallProgressNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpToolCallProgressNotification = { - threadId: string; - turnId: string; - itemId: string; - message: string; -}; diff --git a/src/runtimes/openai/generated/v2/McpToolCallResult.ts b/src/runtimes/openai/generated/v2/McpToolCallResult.ts deleted file mode 100644 index 4bbaa31..0000000 --- a/src/runtimes/openai/generated/v2/McpToolCallResult.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type McpToolCallResult = { - content: Array; - structuredContent: JsonValue | null; - _meta: JsonValue | null; -}; diff --git a/src/runtimes/openai/generated/v2/McpToolCallStatus.ts b/src/runtimes/openai/generated/v2/McpToolCallStatus.ts deleted file mode 100644 index f46bca0..0000000 --- a/src/runtimes/openai/generated/v2/McpToolCallStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpToolCallStatus = "inProgress" | "completed" | "failed"; diff --git a/src/runtimes/openai/generated/v2/MemoryCitation.ts b/src/runtimes/openai/generated/v2/MemoryCitation.ts deleted file mode 100644 index ffc652f..0000000 --- a/src/runtimes/openai/generated/v2/MemoryCitation.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { MemoryCitationEntry } from "./MemoryCitationEntry"; - -export type MemoryCitation = { entries: Array; threadIds: Array }; diff --git a/src/runtimes/openai/generated/v2/MemoryCitationEntry.ts b/src/runtimes/openai/generated/v2/MemoryCitationEntry.ts deleted file mode 100644 index eb74521..0000000 --- a/src/runtimes/openai/generated/v2/MemoryCitationEntry.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MemoryCitationEntry = { - path: string; - lineStart: number; - lineEnd: number; - note: string; -}; diff --git a/src/runtimes/openai/generated/v2/MemoryResetResponse.ts b/src/runtimes/openai/generated/v2/MemoryResetResponse.ts deleted file mode 100644 index d950794..0000000 --- a/src/runtimes/openai/generated/v2/MemoryResetResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MemoryResetResponse = Record; diff --git a/src/runtimes/openai/generated/v2/MergeStrategy.ts b/src/runtimes/openai/generated/v2/MergeStrategy.ts deleted file mode 100644 index 098677f..0000000 --- a/src/runtimes/openai/generated/v2/MergeStrategy.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MergeStrategy = "replace" | "upsert"; diff --git a/src/runtimes/openai/generated/v2/MigrationDetails.ts b/src/runtimes/openai/generated/v2/MigrationDetails.ts deleted file mode 100644 index cec07e8..0000000 --- a/src/runtimes/openai/generated/v2/MigrationDetails.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandMigration } from "./CommandMigration"; -import type { HookMigration } from "./HookMigration"; -import type { McpServerMigration } from "./McpServerMigration"; -import type { PluginsMigration } from "./PluginsMigration"; -import type { SessionMigration } from "./SessionMigration"; -import type { SkillMigration } from "./SkillMigration"; -import type { SubagentMigration } from "./SubagentMigration"; - -export type MigrationDetails = { - plugins: Array; - skills: Array; - sessions: Array; - mcpServers: Array; - hooks: Array; - subagents: Array; - commands: Array; - memory?: Array; -}; diff --git a/src/runtimes/openai/generated/v2/MockExperimentalMethodParams.ts b/src/runtimes/openai/generated/v2/MockExperimentalMethodParams.ts deleted file mode 100644 index f48f796..0000000 --- a/src/runtimes/openai/generated/v2/MockExperimentalMethodParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MockExperimentalMethodParams = { - /** - * Test-only payload field. - */ - value?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/MockExperimentalMethodResponse.ts b/src/runtimes/openai/generated/v2/MockExperimentalMethodResponse.ts deleted file mode 100644 index 02ad930..0000000 --- a/src/runtimes/openai/generated/v2/MockExperimentalMethodResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MockExperimentalMethodResponse = { - /** - * Echoes the input `value`. - */ - echoed: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/Model.ts b/src/runtimes/openai/generated/v2/Model.ts deleted file mode 100644 index f945e53..0000000 --- a/src/runtimes/openai/generated/v2/Model.ts +++ /dev/null @@ -1,40 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { InputModality } from "../InputModality"; -import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; -import type { ModelServiceTier } from "./ModelServiceTier"; -import type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; -import type { MultiAgentVersion } from "./MultiAgentVersion"; -import type { ReasoningEffortOption } from "./ReasoningEffortOption"; - -export type Model = { - id: string; - model: string; - upgrade: string | null; - upgradeInfo: ModelUpgradeInfo | null; - availabilityNux: ModelAvailabilityNux | null; - displayName: string; - description: string; - modelSpecialty: string | null; - hidden: boolean; - supportedReasoningEfforts: Array; - defaultReasoningEffort: ReasoningEffort; - inputModalities: Array; - supportsPersonality: boolean; - /** - * Multi-agent runtime declared by this model, when available. - */ - multiAgentVersion: MultiAgentVersion | null; - /** - * Deprecated: use `serviceTiers` instead. - */ - additionalSpeedTiers: Array; - serviceTiers: Array; - /** - * Catalog default service tier id for this model, when one is configured. - */ - defaultServiceTier: string | null; - isDefault: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ModelAvailabilityNux.ts b/src/runtimes/openai/generated/v2/ModelAvailabilityNux.ts deleted file mode 100644 index 8ea4b6e..0000000 --- a/src/runtimes/openai/generated/v2/ModelAvailabilityNux.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelAvailabilityNux = { message: string }; diff --git a/src/runtimes/openai/generated/v2/ModelListParams.ts b/src/runtimes/openai/generated/v2/ModelListParams.ts deleted file mode 100644 index 568c8ae..0000000 --- a/src/runtimes/openai/generated/v2/ModelListParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a reasonable server-side value. - */ - limit?: number | null; - /** - * When true, include models that are hidden from the default picker list. - */ - includeHidden?: boolean | null; -}; diff --git a/src/runtimes/openai/generated/v2/ModelListResponse.ts b/src/runtimes/openai/generated/v2/ModelListResponse.ts deleted file mode 100644 index ad5077a..0000000 --- a/src/runtimes/openai/generated/v2/ModelListResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Model } from "./Model"; - -export type ModelListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadParams.ts b/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadParams.ts deleted file mode 100644 index 00cbe47..0000000 --- a/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelProviderCapabilitiesReadParams = Record; diff --git a/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadResponse.ts b/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadResponse.ts deleted file mode 100644 index f831613..0000000 --- a/src/runtimes/openai/generated/v2/ModelProviderCapabilitiesReadResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelProviderCapabilitiesReadResponse = { - namespaceTools: boolean; - imageGeneration: boolean; - webSearch: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ModelRerouteReason.ts b/src/runtimes/openai/generated/v2/ModelRerouteReason.ts deleted file mode 100644 index e780e7f..0000000 --- a/src/runtimes/openai/generated/v2/ModelRerouteReason.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelRerouteReason = "highRiskCyberActivity"; diff --git a/src/runtimes/openai/generated/v2/ModelReroutedNotification.ts b/src/runtimes/openai/generated/v2/ModelReroutedNotification.ts deleted file mode 100644 index 3a720a9..0000000 --- a/src/runtimes/openai/generated/v2/ModelReroutedNotification.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ModelRerouteReason } from "./ModelRerouteReason"; - -export type ModelReroutedNotification = { - threadId: string; - turnId: string; - fromModel: string; - toModel: string; - reason: ModelRerouteReason; -}; diff --git a/src/runtimes/openai/generated/v2/ModelSafetyBufferingUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ModelSafetyBufferingUpdatedNotification.ts deleted file mode 100644 index 315d913..0000000 --- a/src/runtimes/openai/generated/v2/ModelSafetyBufferingUpdatedNotification.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelSafetyBufferingUpdatedNotification = { - threadId: string; - turnId: string; - model: string; - useCases: Array; - reasons: Array; - showBufferingUi: boolean; - fasterModel: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ModelServiceTier.ts b/src/runtimes/openai/generated/v2/ModelServiceTier.ts deleted file mode 100644 index b2a8359..0000000 --- a/src/runtimes/openai/generated/v2/ModelServiceTier.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelServiceTier = { id: string; name: string; description: string }; diff --git a/src/runtimes/openai/generated/v2/ModelUpgradeInfo.ts b/src/runtimes/openai/generated/v2/ModelUpgradeInfo.ts deleted file mode 100644 index 957820d..0000000 --- a/src/runtimes/openai/generated/v2/ModelUpgradeInfo.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelUpgradeInfo = { - model: string; - upgradeCopy: string | null; - modelLink: string | null; - migrationMarkdown: string | null; - /** - * Informational Unix timestamp for this upgrade's scheduled retirement, if known. - */ - retirementAt: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ModelVerification.ts b/src/runtimes/openai/generated/v2/ModelVerification.ts deleted file mode 100644 index 00538c0..0000000 --- a/src/runtimes/openai/generated/v2/ModelVerification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelVerification = "trustedAccessForCyber"; diff --git a/src/runtimes/openai/generated/v2/ModelVerificationNotification.ts b/src/runtimes/openai/generated/v2/ModelVerificationNotification.ts deleted file mode 100644 index d328ae3..0000000 --- a/src/runtimes/openai/generated/v2/ModelVerificationNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ModelVerification } from "./ModelVerification"; - -export type ModelVerificationNotification = { - threadId: string; - turnId: string; - verifications: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ModelsRequirements.ts b/src/runtimes/openai/generated/v2/ModelsRequirements.ts deleted file mode 100644 index ed828da..0000000 --- a/src/runtimes/openai/generated/v2/ModelsRequirements.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NewThreadModelDefaults } from "./NewThreadModelDefaults"; - -export type ModelsRequirements = { newThread: NewThreadModelDefaults | null }; diff --git a/src/runtimes/openai/generated/v2/MultiAgentVersion.ts b/src/runtimes/openai/generated/v2/MultiAgentVersion.ts deleted file mode 100644 index b71323e..0000000 --- a/src/runtimes/openai/generated/v2/MultiAgentVersion.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Multi-agent runtime supported by a model. - */ -export type MultiAgentVersion = "disabled" | "v1" | "v2"; diff --git a/src/runtimes/openai/generated/v2/NetworkApprovalContext.ts b/src/runtimes/openai/generated/v2/NetworkApprovalContext.ts deleted file mode 100644 index ef7ab28..0000000 --- a/src/runtimes/openai/generated/v2/NetworkApprovalContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; - -export type NetworkApprovalContext = { host: string; protocol: NetworkApprovalProtocol }; diff --git a/src/runtimes/openai/generated/v2/NetworkApprovalProtocol.ts b/src/runtimes/openai/generated/v2/NetworkApprovalProtocol.ts deleted file mode 100644 index 9dd4066..0000000 --- a/src/runtimes/openai/generated/v2/NetworkApprovalProtocol.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkApprovalProtocol = "http" | "https" | "socks5Tcp" | "socks5Udp"; diff --git a/src/runtimes/openai/generated/v2/NetworkDomainPermission.ts b/src/runtimes/openai/generated/v2/NetworkDomainPermission.ts deleted file mode 100644 index 2ea4439..0000000 --- a/src/runtimes/openai/generated/v2/NetworkDomainPermission.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkDomainPermission = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/v2/NetworkPolicyAmendment.ts b/src/runtimes/openai/generated/v2/NetworkPolicyAmendment.ts deleted file mode 100644 index 51348de..0000000 --- a/src/runtimes/openai/generated/v2/NetworkPolicyAmendment.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; - -export type NetworkPolicyAmendment = { host: string; action: NetworkPolicyRuleAction }; diff --git a/src/runtimes/openai/generated/v2/NetworkPolicyRuleAction.ts b/src/runtimes/openai/generated/v2/NetworkPolicyRuleAction.ts deleted file mode 100644 index 55ec700..0000000 --- a/src/runtimes/openai/generated/v2/NetworkPolicyRuleAction.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkPolicyRuleAction = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/v2/NetworkRequirements.ts b/src/runtimes/openai/generated/v2/NetworkRequirements.ts deleted file mode 100644 index e88a0c1..0000000 --- a/src/runtimes/openai/generated/v2/NetworkRequirements.ts +++ /dev/null @@ -1,40 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NetworkDomainPermission } from "./NetworkDomainPermission"; -import type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission"; - -export type NetworkRequirements = { - enabled: boolean | null; - httpPort: number | null; - socksPort: number | null; - allowUpstreamProxy: boolean | null; - dangerouslyAllowNonLoopbackProxy: boolean | null; - dangerouslyAllowAllUnixSockets: boolean | null; - /** - * Canonical network permission map for `experimental_network`. - */ - domains: { [key in string]?: NetworkDomainPermission } | null; - /** - * When true, only managed allowlist entries are respected while managed - * network enforcement is active. - */ - managedAllowedDomainsOnly: boolean | null; - /** - * Legacy compatibility view derived from `domains`. - */ - allowedDomains: Array | null; - /** - * Legacy compatibility view derived from `domains`. - */ - deniedDomains: Array | null; - /** - * Canonical unix socket permission map for `experimental_network`. - */ - unixSockets: { [key in string]?: NetworkUnixSocketPermission } | null; - /** - * Legacy compatibility view derived from `unix_sockets`. - */ - allowUnixSockets: Array | null; - allowLocalBinding: boolean | null; -}; diff --git a/src/runtimes/openai/generated/v2/NetworkUnixSocketPermission.ts b/src/runtimes/openai/generated/v2/NetworkUnixSocketPermission.ts deleted file mode 100644 index c5474cb..0000000 --- a/src/runtimes/openai/generated/v2/NetworkUnixSocketPermission.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkUnixSocketPermission = "allow" | "deny"; diff --git a/src/runtimes/openai/generated/v2/NewThreadModelDefaults.ts b/src/runtimes/openai/generated/v2/NewThreadModelDefaults.ts deleted file mode 100644 index f664b37..0000000 --- a/src/runtimes/openai/generated/v2/NewThreadModelDefaults.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReasoningEffort } from "../ReasoningEffort"; - -export type NewThreadModelDefaults = { - model: string | null; - modelReasoningEffort: ReasoningEffort | null; - serviceTier: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/NonSteerableTurnKind.ts b/src/runtimes/openai/generated/v2/NonSteerableTurnKind.ts deleted file mode 100644 index 2624df2..0000000 --- a/src/runtimes/openai/generated/v2/NonSteerableTurnKind.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NonSteerableTurnKind = "review" | "compact"; diff --git a/src/runtimes/openai/generated/v2/OverriddenMetadata.ts b/src/runtimes/openai/generated/v2/OverriddenMetadata.ts deleted file mode 100644 index 04fe461..0000000 --- a/src/runtimes/openai/generated/v2/OverriddenMetadata.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; -import type { ConfigLayerMetadata } from "./ConfigLayerMetadata"; - -export type OverriddenMetadata = { - message: string; - overridingLayer: ConfigLayerMetadata; - effectiveValue: JsonValue; -}; diff --git a/src/runtimes/openai/generated/v2/PatchApplyStatus.ts b/src/runtimes/openai/generated/v2/PatchApplyStatus.ts deleted file mode 100644 index 620be78..0000000 --- a/src/runtimes/openai/generated/v2/PatchApplyStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PatchApplyStatus = "inProgress" | "completed" | "failed" | "declined"; diff --git a/src/runtimes/openai/generated/v2/PatchChangeKind.ts b/src/runtimes/openai/generated/v2/PatchChangeKind.ts deleted file mode 100644 index 41ef25c..0000000 --- a/src/runtimes/openai/generated/v2/PatchChangeKind.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PatchChangeKind = - | { type: "add" } - | { type: "delete" } - | { type: "update"; move_path: string | null }; diff --git a/src/runtimes/openai/generated/v2/PermissionGrantScope.ts b/src/runtimes/openai/generated/v2/PermissionGrantScope.ts deleted file mode 100644 index 8ca127e..0000000 --- a/src/runtimes/openai/generated/v2/PermissionGrantScope.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PermissionGrantScope = "turn" | "session"; diff --git a/src/runtimes/openai/generated/v2/PermissionProfileListParams.ts b/src/runtimes/openai/generated/v2/PermissionProfileListParams.ts deleted file mode 100644 index 151b39b..0000000 --- a/src/runtimes/openai/generated/v2/PermissionProfileListParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PermissionProfileListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to the full result set. - */ - limit?: number | null; - /** - * Optional working directory to resolve project config layers. - */ - cwd?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/PermissionProfileListResponse.ts b/src/runtimes/openai/generated/v2/PermissionProfileListResponse.ts deleted file mode 100644 index eade39b..0000000 --- a/src/runtimes/openai/generated/v2/PermissionProfileListResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PermissionProfileSummary } from "./PermissionProfileSummary"; - -export type PermissionProfileListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/PermissionProfileSummary.ts b/src/runtimes/openai/generated/v2/PermissionProfileSummary.ts deleted file mode 100644 index d775dc6..0000000 --- a/src/runtimes/openai/generated/v2/PermissionProfileSummary.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PermissionProfileSummary = { - /** - * Available permission profile identifier. - */ - id: string; - /** - * Optional user-facing description for display in clients. - */ - description: string | null; - /** - * Whether the effective requirements allow selecting this profile. - */ - allowed: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/PermissionsRequestApprovalParams.ts b/src/runtimes/openai/generated/v2/PermissionsRequestApprovalParams.ts deleted file mode 100644 index 8d050ae..0000000 --- a/src/runtimes/openai/generated/v2/PermissionsRequestApprovalParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { RequestPermissionProfile } from "./RequestPermissionProfile"; - -export type PermissionsRequestApprovalParams = { - threadId: string; - turnId: string; - itemId: string; - environmentId: string | null; - /** - * Unix timestamp (in milliseconds) when this approval request started. - */ - startedAtMs: number; - cwd: AbsolutePathBuf; - reason: string | null; - permissions: RequestPermissionProfile; -}; diff --git a/src/runtimes/openai/generated/v2/PermissionsRequestApprovalResponse.ts b/src/runtimes/openai/generated/v2/PermissionsRequestApprovalResponse.ts deleted file mode 100644 index eb7d5ac..0000000 --- a/src/runtimes/openai/generated/v2/PermissionsRequestApprovalResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GrantedPermissionProfile } from "./GrantedPermissionProfile"; -import type { PermissionGrantScope } from "./PermissionGrantScope"; - -export type PermissionsRequestApprovalResponse = { - permissions: GrantedPermissionProfile; - scope: PermissionGrantScope; - /** - * Review every subsequent command in this turn before normal sandboxed execution. - */ - strictAutoReview?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/PlanDeltaNotification.ts b/src/runtimes/openai/generated/v2/PlanDeltaNotification.ts deleted file mode 100644 index 262f652..0000000 --- a/src/runtimes/openai/generated/v2/PlanDeltaNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should - * not assume concatenated deltas match the completed plan item content. - */ -export type PlanDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; -}; diff --git a/src/runtimes/openai/generated/v2/PluginAuthPolicy.ts b/src/runtimes/openai/generated/v2/PluginAuthPolicy.ts deleted file mode 100644 index 5b90e9c..0000000 --- a/src/runtimes/openai/generated/v2/PluginAuthPolicy.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginAuthPolicy = "ON_INSTALL" | "ON_USE"; diff --git a/src/runtimes/openai/generated/v2/PluginAvailability.ts b/src/runtimes/openai/generated/v2/PluginAvailability.ts deleted file mode 100644 index bec0b88..0000000 --- a/src/runtimes/openai/generated/v2/PluginAvailability.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginAvailability = "AVAILABLE" | "DISABLED_BY_ADMIN"; diff --git a/src/runtimes/openai/generated/v2/PluginDetail.ts b/src/runtimes/openai/generated/v2/PluginDetail.ts deleted file mode 100644 index 37337cd..0000000 --- a/src/runtimes/openai/generated/v2/PluginDetail.ts +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { AppSummary } from "./AppSummary"; -import type { AppTemplateSummary } from "./AppTemplateSummary"; -import type { PluginHookSummary } from "./PluginHookSummary"; -import type { PluginSummary } from "./PluginSummary"; -import type { ScheduledTaskSummary } from "./ScheduledTaskSummary"; -import type { SkillSummary } from "./SkillSummary"; - -export type PluginDetail = { - marketplaceName: string; - marketplacePath: AbsolutePathBuf | null; - summary: PluginSummary; - shareUrl: string | null; - description: string | null; - skills: Array; - hooks: Array; - apps: Array; - appTemplates: Array; - mcpServers: Array; - scheduledTasks: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/PluginDisabledReason.ts b/src/runtimes/openai/generated/v2/PluginDisabledReason.ts deleted file mode 100644 index 80eed0b..0000000 --- a/src/runtimes/openai/generated/v2/PluginDisabledReason.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginDisabledReason = - | "disabled_by_admin" - | "plan_not_eligible" - | "required_app_unavailable" - | "unknown"; diff --git a/src/runtimes/openai/generated/v2/PluginHookSummary.ts b/src/runtimes/openai/generated/v2/PluginHookSummary.ts deleted file mode 100644 index 03fe54a..0000000 --- a/src/runtimes/openai/generated/v2/PluginHookSummary.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HookEventName } from "./HookEventName"; - -export type PluginHookSummary = { key: string; eventName: HookEventName }; diff --git a/src/runtimes/openai/generated/v2/PluginInstallParams.ts b/src/runtimes/openai/generated/v2/PluginInstallParams.ts deleted file mode 100644 index b8e6491..0000000 --- a/src/runtimes/openai/generated/v2/PluginInstallParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type PluginInstallParams = { - marketplacePath?: AbsolutePathBuf | null; - remoteMarketplaceName?: string | null; - /** - * Client-generated identifier used to correlate one installation attempt. - */ - installAttemptId?: string | null; - pluginName: string; -}; diff --git a/src/runtimes/openai/generated/v2/PluginInstallPolicy.ts b/src/runtimes/openai/generated/v2/PluginInstallPolicy.ts deleted file mode 100644 index d624f38..0000000 --- a/src/runtimes/openai/generated/v2/PluginInstallPolicy.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginInstallPolicy = "NOT_AVAILABLE" | "AVAILABLE" | "INSTALLED_BY_DEFAULT"; diff --git a/src/runtimes/openai/generated/v2/PluginInstallPolicySource.ts b/src/runtimes/openai/generated/v2/PluginInstallPolicySource.ts deleted file mode 100644 index caa3962..0000000 --- a/src/runtimes/openai/generated/v2/PluginInstallPolicySource.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginInstallPolicySource = "WORKSPACE_SETTING" | "IMPLICIT_CANONICAL_APP"; diff --git a/src/runtimes/openai/generated/v2/PluginInstallResponse.ts b/src/runtimes/openai/generated/v2/PluginInstallResponse.ts deleted file mode 100644 index af50cee..0000000 --- a/src/runtimes/openai/generated/v2/PluginInstallResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppSummary } from "./AppSummary"; -import type { PluginAuthPolicy } from "./PluginAuthPolicy"; - -export type PluginInstallResponse = { - authPolicy: PluginAuthPolicy; - appsNeedingAuth: Array; -}; diff --git a/src/runtimes/openai/generated/v2/PluginInstalledParams.ts b/src/runtimes/openai/generated/v2/PluginInstalledParams.ts deleted file mode 100644 index 0a3b811..0000000 --- a/src/runtimes/openai/generated/v2/PluginInstalledParams.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type PluginInstalledParams = { - /** - * Optional working directories used to discover repo marketplaces. - */ - cwds?: Array | null; - /** - * Additional uninstalled plugin names that should be returned when present locally. - * This is used by mention surfaces that intentionally expose install entrypoints. - */ - installSuggestionPluginNames?: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/PluginInstalledResponse.ts b/src/runtimes/openai/generated/v2/PluginInstalledResponse.ts deleted file mode 100644 index 470e694..0000000 --- a/src/runtimes/openai/generated/v2/PluginInstalledResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo"; -import type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry"; - -export type PluginInstalledResponse = { - marketplaces: Array; - marketplaceLoadErrors: Array; -}; diff --git a/src/runtimes/openai/generated/v2/PluginInterface.ts b/src/runtimes/openai/generated/v2/PluginInterface.ts deleted file mode 100644 index 4cffe82..0000000 --- a/src/runtimes/openai/generated/v2/PluginInterface.ts +++ /dev/null @@ -1,54 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type PluginInterface = { - displayName: string | null; - shortDescription: string | null; - longDescription: string | null; - developerName: string | null; - category: string | null; - capabilities: Array; - websiteUrl: string | null; - privacyPolicyUrl: string | null; - termsOfServiceUrl: string | null; - /** - * Starter prompts for the plugin. Capped at 3 entries with a maximum of - * 128 characters per entry. - */ - defaultPrompt: Array | null; - brandColor: string | null; - /** - * Local composer icon path, resolved from the installed plugin package. - */ - composerIcon: AbsolutePathBuf | null; - /** - * Remote composer icon URL from the plugin catalog. - */ - composerIconUrl: string | null; - /** - * Local logo path, resolved from the installed plugin package. - */ - logo: AbsolutePathBuf | null; - /** - * Local dark-mode logo path, resolved from the installed plugin package. - */ - logoDark: AbsolutePathBuf | null; - /** - * Remote logo URL from the plugin catalog. - */ - logoUrl: string | null; - /** - * Remote dark-mode logo URL from the plugin catalog. - */ - logoUrlDark: string | null; - /** - * Local screenshot paths, resolved from the installed plugin package. - */ - screenshots: Array; - /** - * Remote screenshot URLs from the plugin catalog. - */ - screenshotUrls: Array; -}; diff --git a/src/runtimes/openai/generated/v2/PluginListMarketplaceKind.ts b/src/runtimes/openai/generated/v2/PluginListMarketplaceKind.ts deleted file mode 100644 index 0c39d2f..0000000 --- a/src/runtimes/openai/generated/v2/PluginListMarketplaceKind.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginListMarketplaceKind = - | "local" - | "vertical" - | "workspace-directory" - | "shared-with-me" - | "created-by-me-remote"; diff --git a/src/runtimes/openai/generated/v2/PluginListParams.ts b/src/runtimes/openai/generated/v2/PluginListParams.ts deleted file mode 100644 index 96c6bf9..0000000 --- a/src/runtimes/openai/generated/v2/PluginListParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { PluginListMarketplaceKind } from "./PluginListMarketplaceKind"; - -export type PluginListParams = { - /** - * Optional working directories used to discover repo marketplaces. When omitted, - * only home-scoped marketplaces and the official curated marketplace are considered. - */ - cwds?: Array | null; - /** - * Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus - * the default remote catalog when enabled by feature flag. - */ - marketplaceKinds?: Array | null; - /** - * Whether the client requests a fresh remote plugin catalog fetch. - */ - forceRefetch?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/PluginListResponse.ts b/src/runtimes/openai/generated/v2/PluginListResponse.ts deleted file mode 100644 index c4ecfca..0000000 --- a/src/runtimes/openai/generated/v2/PluginListResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo"; -import type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry"; - -export type PluginListResponse = { - marketplaces: Array; - marketplaceLoadErrors: Array; - featuredPluginIds: Array; -}; diff --git a/src/runtimes/openai/generated/v2/PluginMarketplaceEntry.ts b/src/runtimes/openai/generated/v2/PluginMarketplaceEntry.ts deleted file mode 100644 index d832799..0000000 --- a/src/runtimes/openai/generated/v2/PluginMarketplaceEntry.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { MarketplaceInterface } from "./MarketplaceInterface"; -import type { PluginSummary } from "./PluginSummary"; - -export type PluginMarketplaceEntry = { - name: string; - /** - * Local marketplace file path when the marketplace is backed by a local file. - * Remote-only catalog marketplaces do not have a local path. - */ - path: AbsolutePathBuf | null; - interface: MarketplaceInterface | null; - plugins: Array; -}; diff --git a/src/runtimes/openai/generated/v2/PluginReadParams.ts b/src/runtimes/openai/generated/v2/PluginReadParams.ts deleted file mode 100644 index ac24193..0000000 --- a/src/runtimes/openai/generated/v2/PluginReadParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type PluginReadParams = { - marketplacePath?: AbsolutePathBuf | null; - remoteMarketplaceName?: string | null; - pluginName: string; -}; diff --git a/src/runtimes/openai/generated/v2/PluginReadResponse.ts b/src/runtimes/openai/generated/v2/PluginReadResponse.ts deleted file mode 100644 index 7ef172c..0000000 --- a/src/runtimes/openai/generated/v2/PluginReadResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginDetail } from "./PluginDetail"; - -export type PluginReadResponse = { plugin: PluginDetail }; diff --git a/src/runtimes/openai/generated/v2/PluginSearchParams.ts b/src/runtimes/openai/generated/v2/PluginSearchParams.ts deleted file mode 100644 index f9ce33e..0000000 --- a/src/runtimes/openai/generated/v2/PluginSearchParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { PluginSearchScope } from "./PluginSearchScope"; - -export type PluginSearchParams = { - searchTerm: string; - scope?: PluginSearchScope | null; - cwds?: Array | null; - cursor?: string | null; - limit?: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/PluginSearchResponse.ts b/src/runtimes/openai/generated/v2/PluginSearchResponse.ts deleted file mode 100644 index 363a570..0000000 --- a/src/runtimes/openai/generated/v2/PluginSearchResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginSearchResult } from "./PluginSearchResult"; - -export type PluginSearchResponse = { data: Array; nextCursor: string | null }; diff --git a/src/runtimes/openai/generated/v2/PluginSearchResult.ts b/src/runtimes/openai/generated/v2/PluginSearchResult.ts deleted file mode 100644 index 6ab18b0..0000000 --- a/src/runtimes/openai/generated/v2/PluginSearchResult.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { PluginSummary } from "./PluginSummary"; - -export type PluginSearchResult = { - plugin: PluginSummary; - marketplaceName: string; - marketplacePath: AbsolutePathBuf | null; -}; diff --git a/src/runtimes/openai/generated/v2/PluginSearchScope.ts b/src/runtimes/openai/generated/v2/PluginSearchScope.ts deleted file mode 100644 index 0def10b..0000000 --- a/src/runtimes/openai/generated/v2/PluginSearchScope.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginSearchScope = "global" | "workspace" | "personal"; diff --git a/src/runtimes/openai/generated/v2/PluginShareCheckoutParams.ts b/src/runtimes/openai/generated/v2/PluginShareCheckoutParams.ts deleted file mode 100644 index 49e20aa..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareCheckoutParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareCheckoutParams = { remotePluginId: string }; diff --git a/src/runtimes/openai/generated/v2/PluginShareCheckoutResponse.ts b/src/runtimes/openai/generated/v2/PluginShareCheckoutResponse.ts deleted file mode 100644 index d320bb6..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareCheckoutResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type PluginShareCheckoutResponse = { - remotePluginId: string; - pluginId: string; - pluginName: string; - pluginPath: AbsolutePathBuf; - marketplaceName: string; - marketplacePath: AbsolutePathBuf; - remoteVersion: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/PluginShareContext.ts b/src/runtimes/openai/generated/v2/PluginShareContext.ts deleted file mode 100644 index 47daff1..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareContext.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginShareDiscoverability } from "./PluginShareDiscoverability"; -import type { PluginSharePrincipal } from "./PluginSharePrincipal"; - -export type PluginShareContext = { - remotePluginId: string; - /** - * Version of the remote shared plugin release when available. - */ - remoteVersion: string | null; - discoverability: PluginShareDiscoverability | null; - shareUrl: string | null; - creatorAccountUserId: string | null; - creatorName: string | null; - sharePrincipals: Array | null; - canPublishToWorkspace: boolean | null; -}; diff --git a/src/runtimes/openai/generated/v2/PluginShareDeleteParams.ts b/src/runtimes/openai/generated/v2/PluginShareDeleteParams.ts deleted file mode 100644 index 092ac6c..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareDeleteParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareDeleteParams = { remotePluginId: string }; diff --git a/src/runtimes/openai/generated/v2/PluginShareDeleteResponse.ts b/src/runtimes/openai/generated/v2/PluginShareDeleteResponse.ts deleted file mode 100644 index 2310268..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareDeleteResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareDeleteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/PluginShareDiscoverability.ts b/src/runtimes/openai/generated/v2/PluginShareDiscoverability.ts deleted file mode 100644 index 8c22421..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareDiscoverability.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareDiscoverability = "LISTED" | "UNLISTED" | "PRIVATE"; diff --git a/src/runtimes/openai/generated/v2/PluginShareListItem.ts b/src/runtimes/openai/generated/v2/PluginShareListItem.ts deleted file mode 100644 index 3399548..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareListItem.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { PluginSummary } from "./PluginSummary"; - -export type PluginShareListItem = { - plugin: PluginSummary; - localPluginPath: AbsolutePathBuf | null; -}; diff --git a/src/runtimes/openai/generated/v2/PluginShareListParams.ts b/src/runtimes/openai/generated/v2/PluginShareListParams.ts deleted file mode 100644 index 167ace7..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareListParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareListParams = Record; diff --git a/src/runtimes/openai/generated/v2/PluginShareListResponse.ts b/src/runtimes/openai/generated/v2/PluginShareListResponse.ts deleted file mode 100644 index 998ec88..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareListResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginShareListItem } from "./PluginShareListItem"; - -export type PluginShareListResponse = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/PluginSharePrincipal.ts b/src/runtimes/openai/generated/v2/PluginSharePrincipal.ts deleted file mode 100644 index 2014a36..0000000 --- a/src/runtimes/openai/generated/v2/PluginSharePrincipal.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginSharePrincipalRole } from "./PluginSharePrincipalRole"; -import type { PluginSharePrincipalType } from "./PluginSharePrincipalType"; - -export type PluginSharePrincipal = { - principalType: PluginSharePrincipalType; - principalId: string; - role: PluginSharePrincipalRole; - name: string; -}; diff --git a/src/runtimes/openai/generated/v2/PluginSharePrincipalRole.ts b/src/runtimes/openai/generated/v2/PluginSharePrincipalRole.ts deleted file mode 100644 index 0a022a0..0000000 --- a/src/runtimes/openai/generated/v2/PluginSharePrincipalRole.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginSharePrincipalRole = "reader" | "editor" | "owner"; diff --git a/src/runtimes/openai/generated/v2/PluginSharePrincipalType.ts b/src/runtimes/openai/generated/v2/PluginSharePrincipalType.ts deleted file mode 100644 index e54c129..0000000 --- a/src/runtimes/openai/generated/v2/PluginSharePrincipalType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginSharePrincipalType = "user" | "group" | "workspace"; diff --git a/src/runtimes/openai/generated/v2/PluginShareSaveParams.ts b/src/runtimes/openai/generated/v2/PluginShareSaveParams.ts deleted file mode 100644 index c2a5451..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareSaveParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { PluginShareDiscoverability } from "./PluginShareDiscoverability"; -import type { PluginShareTarget } from "./PluginShareTarget"; - -export type PluginShareSaveParams = { - pluginPath: AbsolutePathBuf; - remotePluginId?: string | null; - discoverability?: PluginShareDiscoverability | null; - shareTargets?: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/PluginShareSaveResponse.ts b/src/runtimes/openai/generated/v2/PluginShareSaveResponse.ts deleted file mode 100644 index a97f450..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareSaveResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareSaveResponse = { - remotePluginId: string; - shareUrl: string; - canPublishToWorkspace: boolean | null; -}; diff --git a/src/runtimes/openai/generated/v2/PluginShareTarget.ts b/src/runtimes/openai/generated/v2/PluginShareTarget.ts deleted file mode 100644 index 945607c..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareTarget.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginSharePrincipalType } from "./PluginSharePrincipalType"; -import type { PluginShareTargetRole } from "./PluginShareTargetRole"; - -export type PluginShareTarget = { - principalType: PluginSharePrincipalType; - principalId: string; - role: PluginShareTargetRole; -}; diff --git a/src/runtimes/openai/generated/v2/PluginShareTargetRole.ts b/src/runtimes/openai/generated/v2/PluginShareTargetRole.ts deleted file mode 100644 index 95eee17..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareTargetRole.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareTargetRole = "reader" | "editor"; diff --git a/src/runtimes/openai/generated/v2/PluginShareUpdateDiscoverability.ts b/src/runtimes/openai/generated/v2/PluginShareUpdateDiscoverability.ts deleted file mode 100644 index 767acae..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareUpdateDiscoverability.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareUpdateDiscoverability = "UNLISTED" | "PRIVATE" | "LISTED"; diff --git a/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsParams.ts b/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsParams.ts deleted file mode 100644 index 9d7ba4a..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginShareTarget } from "./PluginShareTarget"; -import type { PluginShareUpdateDiscoverability } from "./PluginShareUpdateDiscoverability"; - -export type PluginShareUpdateTargetsParams = { - remotePluginId: string; - discoverability: PluginShareUpdateDiscoverability; - shareTargets: Array; -}; diff --git a/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsResponse.ts b/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsResponse.ts deleted file mode 100644 index ba991f5..0000000 --- a/src/runtimes/openai/generated/v2/PluginShareUpdateTargetsResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginShareDiscoverability } from "./PluginShareDiscoverability"; -import type { PluginSharePrincipal } from "./PluginSharePrincipal"; - -export type PluginShareUpdateTargetsResponse = { - principals: Array; - discoverability: PluginShareDiscoverability; -}; diff --git a/src/runtimes/openai/generated/v2/PluginSkillReadParams.ts b/src/runtimes/openai/generated/v2/PluginSkillReadParams.ts deleted file mode 100644 index 5145b2c..0000000 --- a/src/runtimes/openai/generated/v2/PluginSkillReadParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginSkillReadParams = { - remoteMarketplaceName: string; - remotePluginId: string; - skillName: string; -}; diff --git a/src/runtimes/openai/generated/v2/PluginSkillReadResponse.ts b/src/runtimes/openai/generated/v2/PluginSkillReadResponse.ts deleted file mode 100644 index 98007bd..0000000 --- a/src/runtimes/openai/generated/v2/PluginSkillReadResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginSkillReadResponse = { contents: string | null }; diff --git a/src/runtimes/openai/generated/v2/PluginSource.ts b/src/runtimes/openai/generated/v2/PluginSource.ts deleted file mode 100644 index cf2eb27..0000000 --- a/src/runtimes/openai/generated/v2/PluginSource.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type PluginSource = - | { type: "local"; path: AbsolutePathBuf } - | { type: "git"; url: string; path: string | null; refName: string | null; sha: string | null } - | { - type: "npm"; - package: string; - /** - * Optional npm version or version range. - */ - version: string | null; - /** - * Optional HTTPS registry URL. Authentication stays in the user's npm config. - */ - registry: string | null; - } - | { type: "remote" }; diff --git a/src/runtimes/openai/generated/v2/PluginSummary.ts b/src/runtimes/openai/generated/v2/PluginSummary.ts deleted file mode 100644 index 5a7552c..0000000 --- a/src/runtimes/openai/generated/v2/PluginSummary.ts +++ /dev/null @@ -1,57 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginAuthPolicy } from "./PluginAuthPolicy"; -import type { PluginAvailability } from "./PluginAvailability"; -import type { PluginDisabledReason } from "./PluginDisabledReason"; -import type { PluginInstallPolicy } from "./PluginInstallPolicy"; -import type { PluginInstallPolicySource } from "./PluginInstallPolicySource"; -import type { PluginInterface } from "./PluginInterface"; -import type { PluginShareContext } from "./PluginShareContext"; -import type { PluginSource } from "./PluginSource"; - -export type PluginSummary = { - id: string; - /** - * Backend remote plugin identifier when available. - */ - remotePluginId: string | null; - /** - * Version advertised by the remote marketplace backend when available. - */ - version: string | null; - /** - * Version of the locally materialized plugin package when available. - */ - localVersion: string | null; - name: string; - /** - * Remote sharing context associated with this plugin when available. - */ - shareContext: PluginShareContext | null; - source: PluginSource; - installed: boolean; - /** - * Unix timestamp in seconds when the remote plugin was installed, when available. - */ - installedAt: number | null; - enabled: boolean; - installPolicy: PluginInstallPolicy; - installPolicySource: PluginInstallPolicySource | null; - mustShowInstallationInterstitial: boolean | null; - authPolicy: PluginAuthPolicy; - /** - * Availability state for installing and using the plugin. - */ - availability: PluginAvailability; - /** - * Why the remote plugin is unavailable, when provided by plugin-service. - */ - disabledReason: PluginDisabledReason | null; - /** - * Raw plugin-service plan identifiers eligible to install the plugin. - */ - eligiblePlanTypes: Array | null; - interface: PluginInterface | null; - keywords: Array; -}; diff --git a/src/runtimes/openai/generated/v2/PluginUninstallParams.ts b/src/runtimes/openai/generated/v2/PluginUninstallParams.ts deleted file mode 100644 index 250ee7d..0000000 --- a/src/runtimes/openai/generated/v2/PluginUninstallParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginUninstallParams = { pluginId: string }; diff --git a/src/runtimes/openai/generated/v2/PluginUninstallResponse.ts b/src/runtimes/openai/generated/v2/PluginUninstallResponse.ts deleted file mode 100644 index 5d02c2f..0000000 --- a/src/runtimes/openai/generated/v2/PluginUninstallResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginUninstallResponse = Record; diff --git a/src/runtimes/openai/generated/v2/PluginsMigration.ts b/src/runtimes/openai/generated/v2/PluginsMigration.ts deleted file mode 100644 index 3d882cf..0000000 --- a/src/runtimes/openai/generated/v2/PluginsMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginsMigration = { marketplaceName: string; pluginNames: Array }; diff --git a/src/runtimes/openai/generated/v2/ProcessExitedNotification.ts b/src/runtimes/openai/generated/v2/ProcessExitedNotification.ts deleted file mode 100644 index 16d4952..0000000 --- a/src/runtimes/openai/generated/v2/ProcessExitedNotification.ts +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Final process exit notification for `process/spawn`. - */ -export type ProcessExitedNotification = { - /** - * Client-supplied, connection-scoped `processHandle` from `process/spawn`. - */ - processHandle: string; - /** - * Process exit code. - */ - exitCode: number; - /** - * Buffered stdout capture. - * - * Empty when stdout was streamed via `process/outputDelta`. - */ - stdout: string; - /** - * Whether stdout reached `outputBytesCap`. - * - * In streaming mode, stdout is empty and cap state is also reported on the - * final stdout `process/outputDelta` notification. - */ - stdoutCapReached: boolean; - /** - * Buffered stderr capture. - * - * Empty when stderr was streamed via `process/outputDelta`. - */ - stderr: string; - /** - * Whether stderr reached `outputBytesCap`. - * - * In streaming mode, stderr is empty and cap state is also reported on the - * final stderr `process/outputDelta` notification. - */ - stderrCapReached: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ProcessKillParams.ts b/src/runtimes/openai/generated/v2/ProcessKillParams.ts deleted file mode 100644 index 51e9603..0000000 --- a/src/runtimes/openai/generated/v2/ProcessKillParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Terminate a running `process/spawn` session. - */ -export type ProcessKillParams = { - /** - * Client-supplied, connection-scoped `processHandle` from `process/spawn`. - */ - processHandle: string; -}; diff --git a/src/runtimes/openai/generated/v2/ProcessKillResponse.ts b/src/runtimes/openai/generated/v2/ProcessKillResponse.ts deleted file mode 100644 index d1bd824..0000000 --- a/src/runtimes/openai/generated/v2/ProcessKillResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Empty success response for `process/kill`. - */ -export type ProcessKillResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProcessOutputDeltaNotification.ts b/src/runtimes/openai/generated/v2/ProcessOutputDeltaNotification.ts deleted file mode 100644 index eeaeec7..0000000 --- a/src/runtimes/openai/generated/v2/ProcessOutputDeltaNotification.ts +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ProcessOutputStream } from "./ProcessOutputStream"; - -/** - * Base64-encoded output chunk emitted for a streaming `process/spawn` request. - */ -export type ProcessOutputDeltaNotification = { - /** - * Client-supplied, connection-scoped `processHandle` from `process/spawn`. - */ - processHandle: string; - /** - * Output stream this chunk belongs to. - */ - stream: ProcessOutputStream; - /** - * Base64-encoded output bytes. - */ - deltaBase64: string; - /** - * True on the final streamed chunk for this stream when output was - * truncated by `outputBytesCap`. - */ - capReached: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ProcessOutputStream.ts b/src/runtimes/openai/generated/v2/ProcessOutputStream.ts deleted file mode 100644 index 1bb550d..0000000 --- a/src/runtimes/openai/generated/v2/ProcessOutputStream.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Stream label for `process/outputDelta` notifications. - */ -export type ProcessOutputStream = "stdout" | "stderr"; diff --git a/src/runtimes/openai/generated/v2/ProcessResizePtyParams.ts b/src/runtimes/openai/generated/v2/ProcessResizePtyParams.ts deleted file mode 100644 index da4cbe4..0000000 --- a/src/runtimes/openai/generated/v2/ProcessResizePtyParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ProcessTerminalSize } from "./ProcessTerminalSize"; - -/** - * Resize a running PTY-backed `process/spawn` session. - */ -export type ProcessResizePtyParams = { - /** - * Client-supplied, connection-scoped `processHandle` from `process/spawn`. - */ - processHandle: string; - /** - * New PTY size in character cells. - */ - size: ProcessTerminalSize; -}; diff --git a/src/runtimes/openai/generated/v2/ProcessResizePtyResponse.ts b/src/runtimes/openai/generated/v2/ProcessResizePtyResponse.ts deleted file mode 100644 index 5d06355..0000000 --- a/src/runtimes/openai/generated/v2/ProcessResizePtyResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Empty success response for `process/resizePty`. - */ -export type ProcessResizePtyResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProcessSpawnParams.ts b/src/runtimes/openai/generated/v2/ProcessSpawnParams.ts deleted file mode 100644 index 9202b60..0000000 --- a/src/runtimes/openai/generated/v2/ProcessSpawnParams.ts +++ /dev/null @@ -1,74 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { ProcessTerminalSize } from "./ProcessTerminalSize"; - -/** - * Spawn a standalone process (argv vector) without a Codex sandbox on the host - * where the app server is running. - * - * `process/spawn` returns after the process has started and the connection-scoped - * `processHandle` has been registered. Process output and exit are reported via - * `process/outputDelta` and `process/exited` notifications. - */ -export type ProcessSpawnParams = { - /** - * Command argv vector. Empty arrays are rejected. - */ - command: Array; - /** - * Client-supplied, connection-scoped process handle. - * - * Duplicate active handles are rejected on the same connection. The same - * handle can be reused after the prior process exits. - */ - processHandle: string; - /** - * Absolute working directory for the process. - */ - cwd: AbsolutePathBuf; - /** - * Enable PTY mode. - * - * This implies `streamStdin` and `streamStdoutStderr`. - */ - tty?: boolean; - /** - * Allow follow-up `process/writeStdin` requests to write stdin bytes. - */ - streamStdin?: boolean; - /** - * Stream stdout/stderr via `process/outputDelta` notifications. - * - * Streamed bytes are not duplicated into the `process/exited` notification. - */ - streamStdoutStderr?: boolean; - /** - * Optional per-stream stdout/stderr capture cap in bytes. - * - * When omitted, the server default applies. Set to `null` to disable the - * cap. - */ - outputBytesCap?: number | null; - /** - * Optional timeout in milliseconds. - * - * When omitted, the server default applies. Set to `null` to disable the - * timeout. - */ - timeoutMs?: number | null; - /** - * Optional environment overrides merged into the app-server process - * environment. - * - * Matching names override inherited values. Set a key to `null` to unset - * an inherited variable. - */ - env?: { [key in string]?: string | null } | null; - /** - * Optional initial PTY size in character cells. Only valid when `tty` is - * true. - */ - size?: ProcessTerminalSize | null; -}; diff --git a/src/runtimes/openai/generated/v2/ProcessSpawnResponse.ts b/src/runtimes/openai/generated/v2/ProcessSpawnResponse.ts deleted file mode 100644 index 57b5222..0000000 --- a/src/runtimes/openai/generated/v2/ProcessSpawnResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `process/spawn`. - */ -export type ProcessSpawnResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProcessTerminalSize.ts b/src/runtimes/openai/generated/v2/ProcessTerminalSize.ts deleted file mode 100644 index 3f93ee6..0000000 --- a/src/runtimes/openai/generated/v2/ProcessTerminalSize.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * PTY size in character cells for `process/spawn` PTY sessions. - */ -export type ProcessTerminalSize = { - /** - * Terminal height in character cells. - */ - rows: number; - /** - * Terminal width in character cells. - */ - cols: number; -}; diff --git a/src/runtimes/openai/generated/v2/ProcessWriteStdinParams.ts b/src/runtimes/openai/generated/v2/ProcessWriteStdinParams.ts deleted file mode 100644 index 853145f..0000000 --- a/src/runtimes/openai/generated/v2/ProcessWriteStdinParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Write stdin bytes to a running `process/spawn` session, close stdin, or - * both. - */ -export type ProcessWriteStdinParams = { - /** - * Client-supplied, connection-scoped `processHandle` from `process/spawn`. - */ - processHandle: string; - /** - * Optional base64-encoded stdin bytes to write. - */ - deltaBase64?: string | null; - /** - * Close stdin after writing `deltaBase64`, if present. - */ - closeStdin?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ProcessWriteStdinResponse.ts b/src/runtimes/openai/generated/v2/ProcessWriteStdinResponse.ts deleted file mode 100644 index 29ba811..0000000 --- a/src/runtimes/openai/generated/v2/ProcessWriteStdinResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Empty success response for `process/writeStdin`. - */ -export type ProcessWriteStdinResponse = Record; diff --git a/src/runtimes/openai/generated/v2/Project.ts b/src/runtimes/openai/generated/v2/Project.ts deleted file mode 100644 index 7d5cebb..0000000 --- a/src/runtimes/openai/generated/v2/Project.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ProjectRoot } from "./ProjectRoot"; - -export type Project = { - id: string; - name: string; - roots: Array; - metadata: { [key in string]?: string }; - position: number; - createdAt: number; - updatedAt: number; -}; diff --git a/src/runtimes/openai/generated/v2/ProjectChangeType.ts b/src/runtimes/openai/generated/v2/ProjectChangeType.ts deleted file mode 100644 index 7c589c0..0000000 --- a/src/runtimes/openai/generated/v2/ProjectChangeType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ProjectChangeType = "created" | "updated" | "deleted"; diff --git a/src/runtimes/openai/generated/v2/ProjectChangedNotification.ts b/src/runtimes/openai/generated/v2/ProjectChangedNotification.ts deleted file mode 100644 index ce58167..0000000 --- a/src/runtimes/openai/generated/v2/ProjectChangedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ProjectChangeType } from "./ProjectChangeType"; - -export type ProjectChangedNotification = { projectId: string; changeType: ProjectChangeType }; diff --git a/src/runtimes/openai/generated/v2/ProjectCreateParams.ts b/src/runtimes/openai/generated/v2/ProjectCreateParams.ts deleted file mode 100644 index f2760db..0000000 --- a/src/runtimes/openai/generated/v2/ProjectCreateParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ProjectRoot } from "./ProjectRoot"; - -export type ProjectCreateParams = { - name: string; - roots: Array; - metadata?: { [key in string]?: string } | null; - idempotencyKey: string; -}; diff --git a/src/runtimes/openai/generated/v2/ProjectCreateResponse.ts b/src/runtimes/openai/generated/v2/ProjectCreateResponse.ts deleted file mode 100644 index d22e491..0000000 --- a/src/runtimes/openai/generated/v2/ProjectCreateResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Project } from "./Project"; - -export type ProjectCreateResponse = { project: Project }; diff --git a/src/runtimes/openai/generated/v2/ProjectDeleteParams.ts b/src/runtimes/openai/generated/v2/ProjectDeleteParams.ts deleted file mode 100644 index 5a079c9..0000000 --- a/src/runtimes/openai/generated/v2/ProjectDeleteParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ProjectDeleteParams = { projectId: string }; diff --git a/src/runtimes/openai/generated/v2/ProjectDeleteResponse.ts b/src/runtimes/openai/generated/v2/ProjectDeleteResponse.ts deleted file mode 100644 index 772fd2d..0000000 --- a/src/runtimes/openai/generated/v2/ProjectDeleteResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ProjectDeleteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProjectImportParams.ts b/src/runtimes/openai/generated/v2/ProjectImportParams.ts deleted file mode 100644 index c40226c..0000000 --- a/src/runtimes/openai/generated/v2/ProjectImportParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ProjectRoot } from "./ProjectRoot"; - -export type ProjectImportParams = { - name: string; - roots: Array; - metadata?: { [key in string]?: string } | null; - threads?: Array | null; - idempotencyKey: string; -}; diff --git a/src/runtimes/openai/generated/v2/ProjectImportResponse.ts b/src/runtimes/openai/generated/v2/ProjectImportResponse.ts deleted file mode 100644 index 90d050c..0000000 --- a/src/runtimes/openai/generated/v2/ProjectImportResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Project } from "./Project"; - -export type ProjectImportResponse = { project: Project }; diff --git a/src/runtimes/openai/generated/v2/ProjectListParams.ts b/src/runtimes/openai/generated/v2/ProjectListParams.ts deleted file mode 100644 index ff3c49c..0000000 --- a/src/runtimes/openai/generated/v2/ProjectListParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ProjectListParams = { cursor?: string | null; limit?: number | null }; diff --git a/src/runtimes/openai/generated/v2/ProjectListResponse.ts b/src/runtimes/openai/generated/v2/ProjectListResponse.ts deleted file mode 100644 index 1fbd21e..0000000 --- a/src/runtimes/openai/generated/v2/ProjectListResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Project } from "./Project"; - -export type ProjectListResponse = { data: Array; nextCursor: string | null }; diff --git a/src/runtimes/openai/generated/v2/ProjectMoveParams.ts b/src/runtimes/openai/generated/v2/ProjectMoveParams.ts deleted file mode 100644 index 4393826..0000000 --- a/src/runtimes/openai/generated/v2/ProjectMoveParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ProjectMoveParams = { projectId: string; beforeProjectId?: string | null }; diff --git a/src/runtimes/openai/generated/v2/ProjectMoveResponse.ts b/src/runtimes/openai/generated/v2/ProjectMoveResponse.ts deleted file mode 100644 index 38a1485..0000000 --- a/src/runtimes/openai/generated/v2/ProjectMoveResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ProjectMoveResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ProjectReadParams.ts b/src/runtimes/openai/generated/v2/ProjectReadParams.ts deleted file mode 100644 index 90ffa2b..0000000 --- a/src/runtimes/openai/generated/v2/ProjectReadParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ProjectReadParams = { projectId: string }; diff --git a/src/runtimes/openai/generated/v2/ProjectReadResponse.ts b/src/runtimes/openai/generated/v2/ProjectReadResponse.ts deleted file mode 100644 index 1cb85a8..0000000 --- a/src/runtimes/openai/generated/v2/ProjectReadResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Project } from "./Project"; - -export type ProjectReadResponse = { project: Project }; diff --git a/src/runtimes/openai/generated/v2/ProjectRoot.ts b/src/runtimes/openai/generated/v2/ProjectRoot.ts deleted file mode 100644 index 8be788b..0000000 --- a/src/runtimes/openai/generated/v2/ProjectRoot.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type ProjectRoot = { path: AbsolutePathBuf }; diff --git a/src/runtimes/openai/generated/v2/ProjectUpdateParams.ts b/src/runtimes/openai/generated/v2/ProjectUpdateParams.ts deleted file mode 100644 index 062fec1..0000000 --- a/src/runtimes/openai/generated/v2/ProjectUpdateParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ProjectRoot } from "./ProjectRoot"; - -export type ProjectUpdateParams = { - projectId: string; - name?: string | null; - roots?: Array | null; - metadata?: { [key in string]?: string } | null; -}; diff --git a/src/runtimes/openai/generated/v2/ProjectUpdateResponse.ts b/src/runtimes/openai/generated/v2/ProjectUpdateResponse.ts deleted file mode 100644 index 8d42e97..0000000 --- a/src/runtimes/openai/generated/v2/ProjectUpdateResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Project } from "./Project"; - -export type ProjectUpdateResponse = { project: Project }; diff --git a/src/runtimes/openai/generated/v2/QueuedSubmission.ts b/src/runtimes/openai/generated/v2/QueuedSubmission.ts deleted file mode 100644 index 80b300c..0000000 --- a/src/runtimes/openai/generated/v2/QueuedSubmission.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { UserInput } from "./UserInput"; - -export type QueuedSubmission = { id: string; input: Array; clientUserMessageId: string }; diff --git a/src/runtimes/openai/generated/v2/RateLimitReachedType.ts b/src/runtimes/openai/generated/v2/RateLimitReachedType.ts deleted file mode 100644 index 84bb0b5..0000000 --- a/src/runtimes/openai/generated/v2/RateLimitReachedType.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RateLimitReachedType = - | "rate_limit_reached" - | "workspace_owner_credits_depleted" - | "workspace_member_credits_depleted" - | "workspace_owner_usage_limit_reached" - | "workspace_member_usage_limit_reached"; diff --git a/src/runtimes/openai/generated/v2/RateLimitResetCredit.ts b/src/runtimes/openai/generated/v2/RateLimitResetCredit.ts deleted file mode 100644 index fc8ad41..0000000 --- a/src/runtimes/openai/generated/v2/RateLimitResetCredit.ts +++ /dev/null @@ -1,30 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus"; -import type { RateLimitResetType } from "./RateLimitResetType"; - -export type RateLimitResetCredit = { - /** - * Opaque backend identifier for this reset credit. - */ - id: string; - resetType: RateLimitResetType; - status: RateLimitResetCreditStatus; - /** - * Unix timestamp in seconds when the credit was granted. - */ - grantedAt: number; - /** - * Unix timestamp in seconds when the credit expires, or `null` if it does not expire. - */ - expiresAt: number | null; - /** - * Backend-provided display title for this credit, or `null` when unavailable. - */ - title: string | null; - /** - * Backend-provided display description for this credit, or `null` when unavailable. - */ - description: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/RateLimitResetCreditStatus.ts b/src/runtimes/openai/generated/v2/RateLimitResetCreditStatus.ts deleted file mode 100644 index fa15861..0000000 --- a/src/runtimes/openai/generated/v2/RateLimitResetCreditStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RateLimitResetCreditStatus = "available" | "redeeming" | "redeemed" | "unknown"; diff --git a/src/runtimes/openai/generated/v2/RateLimitResetCreditsSummary.ts b/src/runtimes/openai/generated/v2/RateLimitResetCreditsSummary.ts deleted file mode 100644 index 4fb121c..0000000 --- a/src/runtimes/openai/generated/v2/RateLimitResetCreditsSummary.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RateLimitResetCredit } from "./RateLimitResetCredit"; - -export type RateLimitResetCreditsSummary = { - availableCount: bigint; - /** - * Detail rows for available reset credits, when the backend provides them. - * - * `null` means only `availableCount` is known, while an empty array means details were fetched - * and no available credits were returned. The backend may cap this list, so its length can be - * less than `availableCount`. - */ - credits: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/RateLimitResetType.ts b/src/runtimes/openai/generated/v2/RateLimitResetType.ts deleted file mode 100644 index 718145b..0000000 --- a/src/runtimes/openai/generated/v2/RateLimitResetType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RateLimitResetType = "codexRateLimits" | "unknown"; diff --git a/src/runtimes/openai/generated/v2/RateLimitSnapshot.ts b/src/runtimes/openai/generated/v2/RateLimitSnapshot.ts deleted file mode 100644 index 29676de..0000000 --- a/src/runtimes/openai/generated/v2/RateLimitSnapshot.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PlanType } from "../PlanType"; -import type { CreditsSnapshot } from "./CreditsSnapshot"; -import type { RateLimitReachedType } from "./RateLimitReachedType"; -import type { RateLimitWindow } from "./RateLimitWindow"; -import type { SpendControlLimitSnapshot } from "./SpendControlLimitSnapshot"; - -export type RateLimitSnapshot = { - limitId: string | null; - limitName: string | null; - primary: RateLimitWindow | null; - secondary: RateLimitWindow | null; - credits: CreditsSnapshot | null; - individualLimit: SpendControlLimitSnapshot | null; - /** - * Backend-reported spend-control state. `None` is unavailable, not a sparse-update recovery. - */ - spendControlReached: boolean | null; - planType: PlanType | null; - rateLimitReachedType: RateLimitReachedType | null; -}; diff --git a/src/runtimes/openai/generated/v2/RateLimitWindow.ts b/src/runtimes/openai/generated/v2/RateLimitWindow.ts deleted file mode 100644 index c0ad595..0000000 --- a/src/runtimes/openai/generated/v2/RateLimitWindow.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RateLimitWindow = { - usedPercent: number; - windowDurationMins: number | null; - resetsAt: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/RawResponseCompletedNotification.ts b/src/runtimes/openai/generated/v2/RawResponseCompletedNotification.ts deleted file mode 100644 index ccf5169..0000000 --- a/src/runtimes/openai/generated/v2/RawResponseCompletedNotification.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; - -/** - * Internal-only notification containing the exact usage from one upstream - * Responses API completion. - */ -export type RawResponseCompletedNotification = { - threadId: string; - turnId: string; - responseId: string; - usage: TokenUsageBreakdown | null; -}; diff --git a/src/runtimes/openai/generated/v2/RawResponseItemCompletedNotification.ts b/src/runtimes/openai/generated/v2/RawResponseItemCompletedNotification.ts deleted file mode 100644 index b0458cd..0000000 --- a/src/runtimes/openai/generated/v2/RawResponseItemCompletedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ResponseItem } from "../ResponseItem"; - -export type RawResponseItemCompletedNotification = { - threadId: string; - turnId: string; - item: ResponseItem; -}; diff --git a/src/runtimes/openai/generated/v2/ReasoningEffortOption.ts b/src/runtimes/openai/generated/v2/ReasoningEffortOption.ts deleted file mode 100644 index bc26f95..0000000 --- a/src/runtimes/openai/generated/v2/ReasoningEffortOption.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReasoningEffort } from "../ReasoningEffort"; - -export type ReasoningEffortOption = { reasoningEffort: ReasoningEffort; description: string }; diff --git a/src/runtimes/openai/generated/v2/ReasoningSummaryPartAddedNotification.ts b/src/runtimes/openai/generated/v2/ReasoningSummaryPartAddedNotification.ts deleted file mode 100644 index 3ba8282..0000000 --- a/src/runtimes/openai/generated/v2/ReasoningSummaryPartAddedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReasoningSummaryPartAddedNotification = { - threadId: string; - turnId: string; - itemId: string; - summaryIndex: number; -}; diff --git a/src/runtimes/openai/generated/v2/ReasoningSummaryTextDeltaNotification.ts b/src/runtimes/openai/generated/v2/ReasoningSummaryTextDeltaNotification.ts deleted file mode 100644 index 7ce5951..0000000 --- a/src/runtimes/openai/generated/v2/ReasoningSummaryTextDeltaNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReasoningSummaryTextDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; - summaryIndex: number; -}; diff --git a/src/runtimes/openai/generated/v2/ReasoningTextDeltaNotification.ts b/src/runtimes/openai/generated/v2/ReasoningTextDeltaNotification.ts deleted file mode 100644 index 23e9bc4..0000000 --- a/src/runtimes/openai/generated/v2/ReasoningTextDeltaNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReasoningTextDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; - contentIndex: number; -}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClient.ts b/src/runtimes/openai/generated/v2/RemoteControlClient.ts deleted file mode 100644 index c66242d..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlClient.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlClient = { - clientId: string; - displayName: string | null; - deviceType: string | null; - platform: string | null; - osVersion: string | null; - deviceModel: string | null; - appVersion: string | null; - lastSeenAt: bigint | null; -}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsListOrder.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsListOrder.ts deleted file mode 100644 index 7166235..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlClientsListOrder.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlClientsListOrder = "asc" | "desc"; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsListParams.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsListParams.ts deleted file mode 100644 index ba6468a..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlClientsListParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RemoteControlClientsListOrder } from "./RemoteControlClientsListOrder"; - -export type RemoteControlClientsListParams = { - environmentId: string; - cursor?: string | null; - limit?: number | null; - order?: RemoteControlClientsListOrder | null; -}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsListResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsListResponse.ts deleted file mode 100644 index c7261f6..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlClientsListResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RemoteControlClient } from "./RemoteControlClient"; - -export type RemoteControlClientsListResponse = { - data: Array; - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeParams.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeParams.ts deleted file mode 100644 index f3b93a8..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlClientsRevokeParams = { environmentId: string; clientId: string }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeResponse.ts deleted file mode 100644 index d30e90c..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlClientsRevokeResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlClientsRevokeResponse = Record; diff --git a/src/runtimes/openai/generated/v2/RemoteControlConnectionStatus.ts b/src/runtimes/openai/generated/v2/RemoteControlConnectionStatus.ts deleted file mode 100644 index 3e6197f..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlConnectionStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlConnectionStatus = "disabled" | "connecting" | "connected" | "errored"; diff --git a/src/runtimes/openai/generated/v2/RemoteControlDisableParams.ts b/src/runtimes/openai/generated/v2/RemoteControlDisableParams.ts deleted file mode 100644 index ec9af47..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlDisableParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlDisableParams = { ephemeral?: boolean }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlDisableResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlDisableResponse.ts deleted file mode 100644 index 023ee48..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlDisableResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; - -export type RemoteControlDisableResponse = { - status: RemoteControlConnectionStatus; - serverName: string; - installationId: string; - environmentId: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlEnableParams.ts b/src/runtimes/openai/generated/v2/RemoteControlEnableParams.ts deleted file mode 100644 index e724071..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlEnableParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlEnableParams = { ephemeral?: boolean }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlEnableResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlEnableResponse.ts deleted file mode 100644 index c4199f4..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlEnableResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; - -export type RemoteControlEnableResponse = { - status: RemoteControlConnectionStatus; - serverName: string; - installationId: string; - environmentId: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlPairingStartParams.ts b/src/runtimes/openai/generated/v2/RemoteControlPairingStartParams.ts deleted file mode 100644 index 444bc58..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlPairingStartParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlPairingStartParams = { manualCode?: boolean }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlPairingStartResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlPairingStartResponse.ts deleted file mode 100644 index 08b86ad..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlPairingStartResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlPairingStartResponse = { - pairingCode: string; - manualPairingCode: string | null; - environmentId: string; - expiresAt: bigint; -}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlPairingStatusParams.ts b/src/runtimes/openai/generated/v2/RemoteControlPairingStatusParams.ts deleted file mode 100644 index 3e0536a..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlPairingStatusParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlPairingStatusParams = { - pairingCode?: string | null; - manualPairingCode?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlPairingStatusResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlPairingStatusResponse.ts deleted file mode 100644 index 92b301f..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlPairingStatusResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlPairingStatusResponse = { claimed: boolean }; diff --git a/src/runtimes/openai/generated/v2/RemoteControlStatusChangedNotification.ts b/src/runtimes/openai/generated/v2/RemoteControlStatusChangedNotification.ts deleted file mode 100644 index 58b0350..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlStatusChangedNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; - -/** - * Current remote-control connection status and remote identity exposed to clients. - */ -export type RemoteControlStatusChangedNotification = { - status: RemoteControlConnectionStatus; - serverName: string; - installationId: string; - environmentId: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/RemoteControlStatusReadResponse.ts b/src/runtimes/openai/generated/v2/RemoteControlStatusReadResponse.ts deleted file mode 100644 index 2421b29..0000000 --- a/src/runtimes/openai/generated/v2/RemoteControlStatusReadResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; - -export type RemoteControlStatusReadResponse = { - status: RemoteControlConnectionStatus; - serverName: string; - installationId: string; - environmentId: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/RequestPermissionProfile.ts b/src/runtimes/openai/generated/v2/RequestPermissionProfile.ts deleted file mode 100644 index f8350a0..0000000 --- a/src/runtimes/openai/generated/v2/RequestPermissionProfile.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; -import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; - -export type RequestPermissionProfile = { - network: AdditionalNetworkPermissions | null; - fileSystem: AdditionalFileSystemPermissions | null; -}; diff --git a/src/runtimes/openai/generated/v2/ResidencyRequirement.ts b/src/runtimes/openai/generated/v2/ResidencyRequirement.ts deleted file mode 100644 index 1699c84..0000000 --- a/src/runtimes/openai/generated/v2/ResidencyRequirement.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ResidencyRequirement = "us"; diff --git a/src/runtimes/openai/generated/v2/ReviewDelivery.ts b/src/runtimes/openai/generated/v2/ReviewDelivery.ts deleted file mode 100644 index 8fbccd1..0000000 --- a/src/runtimes/openai/generated/v2/ReviewDelivery.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReviewDelivery = "inline" | "detached"; diff --git a/src/runtimes/openai/generated/v2/ReviewStartParams.ts b/src/runtimes/openai/generated/v2/ReviewStartParams.ts deleted file mode 100644 index bd72fad..0000000 --- a/src/runtimes/openai/generated/v2/ReviewStartParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReviewDelivery } from "./ReviewDelivery"; -import type { ReviewTarget } from "./ReviewTarget"; - -export type ReviewStartParams = { - threadId: string; - target: ReviewTarget; - /** - * Where to run the review: inline (default) on the current thread or - * detached on a new thread (returned in `reviewThreadId`). - */ - delivery?: ReviewDelivery | null; -}; diff --git a/src/runtimes/openai/generated/v2/ReviewStartResponse.ts b/src/runtimes/openai/generated/v2/ReviewStartResponse.ts deleted file mode 100644 index 9440202..0000000 --- a/src/runtimes/openai/generated/v2/ReviewStartResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn"; - -export type ReviewStartResponse = { - turn: Turn; - /** - * Identifies the thread where the review runs. - * - * For inline reviews, this is the original thread id. - * For detached reviews, this is the id of the new review thread. - */ - reviewThreadId: string; -}; diff --git a/src/runtimes/openai/generated/v2/ReviewTarget.ts b/src/runtimes/openai/generated/v2/ReviewTarget.ts deleted file mode 100644 index 640a5d8..0000000 --- a/src/runtimes/openai/generated/v2/ReviewTarget.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReviewTarget = - | { type: "uncommittedChanges" } - | { type: "baseBranch"; branch: string } - | { - type: "commit"; - sha: string; - /** - * Optional human-readable label (e.g., commit subject) for UIs. - */ - title: string | null; - } - | { type: "custom"; instructions: string }; diff --git a/src/runtimes/openai/generated/v2/SandboxWorkspaceWrite.ts b/src/runtimes/openai/generated/v2/SandboxWorkspaceWrite.ts deleted file mode 100644 index c7e4cb6..0000000 --- a/src/runtimes/openai/generated/v2/SandboxWorkspaceWrite.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SandboxWorkspaceWrite = { - writable_roots: Array; - network_access: boolean; - exclude_tmpdir_env_var: boolean; - exclude_slash_tmp: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ScheduledTaskSchedule.ts b/src/runtimes/openai/generated/v2/ScheduledTaskSchedule.ts deleted file mode 100644 index 4617cf3..0000000 --- a/src/runtimes/openai/generated/v2/ScheduledTaskSchedule.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ScheduledTaskWeekday } from "./ScheduledTaskWeekday"; - -export type ScheduledTaskSchedule = - | { type: "hourly"; intervalHours: number; days: Array | null } - | { type: "daily"; time: string } - | { type: "weekdays"; time: string } - | { type: "weekly"; days: Array; time: string }; diff --git a/src/runtimes/openai/generated/v2/ScheduledTaskSummary.ts b/src/runtimes/openai/generated/v2/ScheduledTaskSummary.ts deleted file mode 100644 index 218ad80..0000000 --- a/src/runtimes/openai/generated/v2/ScheduledTaskSummary.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ScheduledTaskSchedule } from "./ScheduledTaskSchedule"; - -export type ScheduledTaskSummary = { - key: string; - name: string; - prompt: string; - schedule: ScheduledTaskSchedule; -}; diff --git a/src/runtimes/openai/generated/v2/ScheduledTaskWeekday.ts b/src/runtimes/openai/generated/v2/ScheduledTaskWeekday.ts deleted file mode 100644 index bf21096..0000000 --- a/src/runtimes/openai/generated/v2/ScheduledTaskWeekday.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ScheduledTaskWeekday = "MO" | "TU" | "WE" | "TH" | "FR" | "SA" | "SU"; diff --git a/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailParams.ts b/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailParams.ts deleted file mode 100644 index f8a3ebc..0000000 --- a/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType"; - -export type SendAddCreditsNudgeEmailParams = { creditType: AddCreditsNudgeCreditType }; diff --git a/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailResponse.ts b/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailResponse.ts deleted file mode 100644 index a2e096e..0000000 --- a/src/runtimes/openai/generated/v2/SendAddCreditsNudgeEmailResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus"; - -export type SendAddCreditsNudgeEmailResponse = { status: AddCreditsNudgeEmailStatus }; diff --git a/src/runtimes/openai/generated/v2/ServerDiagnosticsGauge.ts b/src/runtimes/openai/generated/v2/ServerDiagnosticsGauge.ts deleted file mode 100644 index 6ac4563..0000000 --- a/src/runtimes/openai/generated/v2/ServerDiagnosticsGauge.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ServerDiagnosticsGauge = { name: string; value: number }; diff --git a/src/runtimes/openai/generated/v2/ServerDiagnosticsParams.ts b/src/runtimes/openai/generated/v2/ServerDiagnosticsParams.ts deleted file mode 100644 index aa1d659..0000000 --- a/src/runtimes/openai/generated/v2/ServerDiagnosticsParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ServerDiagnosticsParams = Record; diff --git a/src/runtimes/openai/generated/v2/ServerDiagnosticsProcess.ts b/src/runtimes/openai/generated/v2/ServerDiagnosticsProcess.ts deleted file mode 100644 index 2caa629..0000000 --- a/src/runtimes/openai/generated/v2/ServerDiagnosticsProcess.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ServerDiagnosticsProcess = { - id: number; - residentMemoryBytes: number | null; - physicalFootprintBytes: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ServerDiagnosticsResponse.ts b/src/runtimes/openai/generated/v2/ServerDiagnosticsResponse.ts deleted file mode 100644 index 5e1aad6..0000000 --- a/src/runtimes/openai/generated/v2/ServerDiagnosticsResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ServerDiagnosticsGauge } from "./ServerDiagnosticsGauge"; -import type { ServerDiagnosticsProcess } from "./ServerDiagnosticsProcess"; - -export type ServerDiagnosticsResponse = { - process: ServerDiagnosticsProcess; - gauges: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ServerRequestResolvedNotification.ts b/src/runtimes/openai/generated/v2/ServerRequestResolvedNotification.ts deleted file mode 100644 index a8c3645..0000000 --- a/src/runtimes/openai/generated/v2/ServerRequestResolvedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RequestId } from "../RequestId"; - -export type ServerRequestResolvedNotification = { threadId: string; requestId: RequestId }; diff --git a/src/runtimes/openai/generated/v2/SessionMigration.ts b/src/runtimes/openai/generated/v2/SessionMigration.ts deleted file mode 100644 index 79a08cb..0000000 --- a/src/runtimes/openai/generated/v2/SessionMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SessionMigration = { path: string; cwd: string; title: string | null }; diff --git a/src/runtimes/openai/generated/v2/SessionSource.ts b/src/runtimes/openai/generated/v2/SessionSource.ts deleted file mode 100644 index 1d68ade..0000000 --- a/src/runtimes/openai/generated/v2/SessionSource.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SubAgentSource } from "../SubAgentSource"; - -export type SessionSource = - | "cli" - | "vscode" - | "exec" - | "appServer" - | { custom: string } - | { subAgent: SubAgentSource } - | "unknown"; diff --git a/src/runtimes/openai/generated/v2/SkillDependencies.ts b/src/runtimes/openai/generated/v2/SkillDependencies.ts deleted file mode 100644 index 6863d3e..0000000 --- a/src/runtimes/openai/generated/v2/SkillDependencies.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SkillToolDependency } from "./SkillToolDependency"; - -export type SkillDependencies = { tools: Array }; diff --git a/src/runtimes/openai/generated/v2/SkillErrorInfo.ts b/src/runtimes/openai/generated/v2/SkillErrorInfo.ts deleted file mode 100644 index 5d7edcc..0000000 --- a/src/runtimes/openai/generated/v2/SkillErrorInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillErrorInfo = { path: string; message: string }; diff --git a/src/runtimes/openai/generated/v2/SkillInterface.ts b/src/runtimes/openai/generated/v2/SkillInterface.ts deleted file mode 100644 index 42d43ce..0000000 --- a/src/runtimes/openai/generated/v2/SkillInterface.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type SkillInterface = { - displayName?: string; - shortDescription?: string; - iconSmall?: AbsolutePathBuf; - iconLarge?: AbsolutePathBuf; - /** - * Remote small icon URL from the plugin catalog. - */ - iconSmallUrl: string | null; - /** - * Remote large icon URL from the plugin catalog. - */ - iconLargeUrl: string | null; - brandColor?: string; - defaultPrompt?: string; -}; diff --git a/src/runtimes/openai/generated/v2/SkillMetadata.ts b/src/runtimes/openai/generated/v2/SkillMetadata.ts deleted file mode 100644 index 183a6f7..0000000 --- a/src/runtimes/openai/generated/v2/SkillMetadata.ts +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { SkillDependencies } from "./SkillDependencies"; -import type { SkillInterface } from "./SkillInterface"; -import type { SkillScope } from "./SkillScope"; - -export type SkillMetadata = { - name: string; - description: string; - /** - * Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description. - */ - shortDescription?: string; - interface?: SkillInterface; - dependencies?: SkillDependencies; - path: AbsolutePathBuf; - scope: SkillScope; - enabled: boolean; - /** - * Owning plugin ID, matching `PluginSummary.id`, when known. - */ - pluginId: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/SkillMigration.ts b/src/runtimes/openai/generated/v2/SkillMigration.ts deleted file mode 100644 index fd41e56..0000000 --- a/src/runtimes/openai/generated/v2/SkillMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/SkillScope.ts b/src/runtimes/openai/generated/v2/SkillScope.ts deleted file mode 100644 index 997006f..0000000 --- a/src/runtimes/openai/generated/v2/SkillScope.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillScope = "user" | "repo" | "system" | "admin"; diff --git a/src/runtimes/openai/generated/v2/SkillSummary.ts b/src/runtimes/openai/generated/v2/SkillSummary.ts deleted file mode 100644 index 29f28af..0000000 --- a/src/runtimes/openai/generated/v2/SkillSummary.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { SkillInterface } from "./SkillInterface"; - -export type SkillSummary = { - name: string; - description: string; - shortDescription: string | null; - interface: SkillInterface | null; - path: AbsolutePathBuf | null; - enabled: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/SkillToolDependency.ts b/src/runtimes/openai/generated/v2/SkillToolDependency.ts deleted file mode 100644 index 36a30a1..0000000 --- a/src/runtimes/openai/generated/v2/SkillToolDependency.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillToolDependency = { - type: string; - value: string; - description?: string; - transport?: string; - command?: string; - url?: string; -}; diff --git a/src/runtimes/openai/generated/v2/SkillsChangedNotification.ts b/src/runtimes/openai/generated/v2/SkillsChangedNotification.ts deleted file mode 100644 index 23ed93a..0000000 --- a/src/runtimes/openai/generated/v2/SkillsChangedNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Notification emitted when watched local skill files change. - * - * Treat this as an invalidation signal and re-run `skills/list` with the - * client's current parameters when refreshed skill metadata is needed. - */ -export type SkillsChangedNotification = Record; diff --git a/src/runtimes/openai/generated/v2/SkillsConfigWriteParams.ts b/src/runtimes/openai/generated/v2/SkillsConfigWriteParams.ts deleted file mode 100644 index 81cf2cc..0000000 --- a/src/runtimes/openai/generated/v2/SkillsConfigWriteParams.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type SkillsConfigWriteParams = { - /** - * Path-based selector. - */ - path?: AbsolutePathBuf | null; - /** - * Name-based selector. - */ - name?: string | null; - enabled: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/SkillsConfigWriteResponse.ts b/src/runtimes/openai/generated/v2/SkillsConfigWriteResponse.ts deleted file mode 100644 index ba52319..0000000 --- a/src/runtimes/openai/generated/v2/SkillsConfigWriteResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillsConfigWriteResponse = { effectiveEnabled: boolean }; diff --git a/src/runtimes/openai/generated/v2/SkillsExtraRootsSetParams.ts b/src/runtimes/openai/generated/v2/SkillsExtraRootsSetParams.ts deleted file mode 100644 index 4bb556e..0000000 --- a/src/runtimes/openai/generated/v2/SkillsExtraRootsSetParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type SkillsExtraRootsSetParams = { extraRoots: Array }; diff --git a/src/runtimes/openai/generated/v2/SkillsExtraRootsSetResponse.ts b/src/runtimes/openai/generated/v2/SkillsExtraRootsSetResponse.ts deleted file mode 100644 index 63be081..0000000 --- a/src/runtimes/openai/generated/v2/SkillsExtraRootsSetResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillsExtraRootsSetResponse = Record; diff --git a/src/runtimes/openai/generated/v2/SkillsListEntry.ts b/src/runtimes/openai/generated/v2/SkillsListEntry.ts deleted file mode 100644 index 4a0d0b6..0000000 --- a/src/runtimes/openai/generated/v2/SkillsListEntry.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SkillErrorInfo } from "./SkillErrorInfo"; -import type { SkillMetadata } from "./SkillMetadata"; - -export type SkillsListEntry = { - cwd: string; - skills: Array; - errors: Array; -}; diff --git a/src/runtimes/openai/generated/v2/SkillsListParams.ts b/src/runtimes/openai/generated/v2/SkillsListParams.ts deleted file mode 100644 index 9168e75..0000000 --- a/src/runtimes/openai/generated/v2/SkillsListParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillsListParams = { - /** - * When empty, defaults to the current session working directory. - */ - cwds?: Array; - /** - * When true, bypass the skills cache and re-scan skills from disk. - */ - forceReload?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/SkillsListResponse.ts b/src/runtimes/openai/generated/v2/SkillsListResponse.ts deleted file mode 100644 index 6fdb9a7..0000000 --- a/src/runtimes/openai/generated/v2/SkillsListResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SkillsListEntry } from "./SkillsListEntry"; - -export type SkillsListResponse = { data: Array }; diff --git a/src/runtimes/openai/generated/v2/SpendControlLimitSnapshot.ts b/src/runtimes/openai/generated/v2/SpendControlLimitSnapshot.ts deleted file mode 100644 index 865e95b..0000000 --- a/src/runtimes/openai/generated/v2/SpendControlLimitSnapshot.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SpendControlLimitSnapshot = { - limit: string; - used: string; - remainingPercent: number; - resetsAt: number; -}; diff --git a/src/runtimes/openai/generated/v2/StrictReviewRequiredNotification.ts b/src/runtimes/openai/generated/v2/StrictReviewRequiredNotification.ts deleted file mode 100644 index 6a37c51..0000000 --- a/src/runtimes/openai/generated/v2/StrictReviewRequiredNotification.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type StrictReviewRequiredNotification = { - threadId: string; - turnId: string; - /** - * Unix timestamp (in milliseconds) when this review started. - */ - startedAtMs: number; -}; diff --git a/src/runtimes/openai/generated/v2/SubAgentActivityKind.ts b/src/runtimes/openai/generated/v2/SubAgentActivityKind.ts deleted file mode 100644 index 8dcacee..0000000 --- a/src/runtimes/openai/generated/v2/SubAgentActivityKind.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SubAgentActivityKind = "started" | "interacted" | "interrupted" | "completed"; diff --git a/src/runtimes/openai/generated/v2/SubagentMigration.ts b/src/runtimes/openai/generated/v2/SubagentMigration.ts deleted file mode 100644 index b361f8b..0000000 --- a/src/runtimes/openai/generated/v2/SubagentMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SubagentMigration = { name: string }; diff --git a/src/runtimes/openai/generated/v2/TerminalInteractionNotification.ts b/src/runtimes/openai/generated/v2/TerminalInteractionNotification.ts deleted file mode 100644 index 17f4978..0000000 --- a/src/runtimes/openai/generated/v2/TerminalInteractionNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TerminalInteractionNotification = { - threadId: string; - turnId: string; - itemId: string; - processId: string; - stdin: string; -}; diff --git a/src/runtimes/openai/generated/v2/TextPosition.ts b/src/runtimes/openai/generated/v2/TextPosition.ts deleted file mode 100644 index fb6c645..0000000 --- a/src/runtimes/openai/generated/v2/TextPosition.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TextPosition = { - /** - * 1-based line number. - */ - line: number; - /** - * 1-based column number (in Unicode scalar values). - */ - column: number; -}; diff --git a/src/runtimes/openai/generated/v2/TextRange.ts b/src/runtimes/openai/generated/v2/TextRange.ts deleted file mode 100644 index 868ecd1..0000000 --- a/src/runtimes/openai/generated/v2/TextRange.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TextPosition } from "./TextPosition"; - -export type TextRange = { start: TextPosition; end: TextPosition }; diff --git a/src/runtimes/openai/generated/v2/Thread.ts b/src/runtimes/openai/generated/v2/Thread.ts deleted file mode 100644 index e55dbf2..0000000 --- a/src/runtimes/openai/generated/v2/Thread.ts +++ /dev/null @@ -1,127 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { GitInfo } from "./GitInfo"; -import type { SessionSource } from "./SessionSource"; -import type { ThreadExtra } from "./ThreadExtra"; -import type { ThreadHistoryMode } from "./ThreadHistoryMode"; -import type { ThreadSection } from "./ThreadSection"; -import type { ThreadSource } from "./ThreadSource"; -import type { ThreadStatus } from "./ThreadStatus"; -import type { Turn } from "./Turn"; - -export type Thread = { - /** - * Identifier for this thread. Codex-generated thread IDs are UUIDv7. - */ - id: string; - /** - * Optional implementation-specific thread data. - */ - extra: ThreadExtra | null; - /** - * Session id shared by threads that belong to the same session tree. - */ - sessionId: string; - /** - * Source thread id when this thread was created by forking another thread. - */ - forkedFromId: string | null; - /** - * The ID of the parent thread. This will only be set if this thread is a subagent. - */ - parentThreadId: string | null; - /** - * Usually the first user message in the thread, if available. - */ - preview: string; - /** - * Whether the thread is ephemeral and should not be materialized on disk. - */ - ephemeral: boolean; - /** - * The independently persisted section selected for this thread, if any. - */ - section: ThreadSection | null; - /** - * Unix timestamp in seconds when the thread entered its current section. - */ - sectionEnteredAt: number | null; - /** - * Canonical project assignment owned by app-server, if any. - */ - projectId: string | null; - /** - * Persisted thread history contract selected when this thread was created. - */ - historyMode: ThreadHistoryMode; - /** - * Model provider used for this thread (for example, 'openai'). - */ - modelProvider: string; - /** - * Unix timestamp (in seconds) when the thread was created. - */ - createdAt: number; - /** - * Unix timestamp (in seconds) when the thread was last updated. - */ - updatedAt: number; - /** - * Unix timestamp (in seconds) used for thread recency ordering. - */ - recencyAt: number | null; - /** - * Current runtime status for the thread. - */ - status: ThreadStatus; - /** - * [UNSTABLE] Path to the thread on disk. - */ - path: string | null; - /** - * Working directory captured for the thread. - */ - cwd: AbsolutePathBuf; - /** - * Version of the CLI that created the thread. - */ - cliVersion: string; - /** - * Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). - */ - source: SessionSource; - /** - * Whether the app server accepts direct turn input for this loaded thread. - * `None` means the capability is unavailable, such as for an unloaded stored thread. - */ - canAcceptDirectInput: boolean | null; - /** - * Optional analytics source classification for this thread. - */ - threadSource: ThreadSource | null; - /** - * Optional random unique nickname assigned to an AgentControl-spawned sub-agent. - */ - agentNickname: string | null; - /** - * Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. - */ - agentRole: string | null; - /** - * Optional Git metadata captured when the thread was created. - */ - gitInfo: GitInfo | null; - /** - * Optional user-facing thread title. - */ - name: string | null; - /** - * Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` - * (when `includeTurns` is true) responses. - * For all other responses and notifications returning a Thread, - * the turns field will be an empty list. - */ - turns: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadActiveFlag.ts b/src/runtimes/openai/generated/v2/ThreadActiveFlag.ts deleted file mode 100644 index 73c875a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadActiveFlag.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadActiveFlag = "waitingOnApproval" | "waitingOnUserInput"; diff --git a/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionParams.ts b/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionParams.ts deleted file mode 100644 index cc29831..0000000 --- a/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type ThreadApproveGuardianDeniedActionParams = { - threadId: string; - /** - * Serialized `codex_protocol::protocol::GuardianAssessmentEvent`. - */ - event: JsonValue; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionResponse.ts b/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionResponse.ts deleted file mode 100644 index 856bb28..0000000 --- a/src/runtimes/openai/generated/v2/ThreadApproveGuardianDeniedActionResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadApproveGuardianDeniedActionResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadArchiveParams.ts b/src/runtimes/openai/generated/v2/ThreadArchiveParams.ts deleted file mode 100644 index 81d1d47..0000000 --- a/src/runtimes/openai/generated/v2/ThreadArchiveParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadArchiveParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadArchiveResponse.ts b/src/runtimes/openai/generated/v2/ThreadArchiveResponse.ts deleted file mode 100644 index b595426..0000000 --- a/src/runtimes/openai/generated/v2/ThreadArchiveResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadArchiveResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadArchivedNotification.ts b/src/runtimes/openai/generated/v2/ThreadArchivedNotification.ts deleted file mode 100644 index a2e81a7..0000000 --- a/src/runtimes/openai/generated/v2/ThreadArchivedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadArchivedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminal.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminal.ts deleted file mode 100644 index ff49a6c..0000000 --- a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminal.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LegacyAppPathString } from "../LegacyAppPathString"; - -export type ThreadBackgroundTerminal = { - itemId: string; - processId: string; - command: string; - cwd: LegacyAppPathString; - osPid: number | null; - cpuPercent: number | null; - rssKb: bigint | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanResponse.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanResponse.ts deleted file mode 100644 index f531fe0..0000000 --- a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsCleanResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadBackgroundTerminalsCleanResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListParams.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListParams.ts deleted file mode 100644 index fcfcd90..0000000 --- a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadBackgroundTerminalsListParams = { - threadId: string; - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size. - */ - limit?: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListResponse.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListResponse.ts deleted file mode 100644 index 227927e..0000000 --- a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsListResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadBackgroundTerminal } from "./ThreadBackgroundTerminal"; - -export type ThreadBackgroundTerminalsListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateParams.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateParams.ts deleted file mode 100644 index d6f5388..0000000 --- a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadBackgroundTerminalsTerminateParams = { threadId: string; processId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateResponse.ts b/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateResponse.ts deleted file mode 100644 index f08b691..0000000 --- a/src/runtimes/openai/generated/v2/ThreadBackgroundTerminalsTerminateResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadBackgroundTerminalsTerminateResponse = { terminated: boolean }; diff --git a/src/runtimes/openai/generated/v2/ThreadClosedNotification.ts b/src/runtimes/openai/generated/v2/ThreadClosedNotification.ts deleted file mode 100644 index 9efd8c2..0000000 --- a/src/runtimes/openai/generated/v2/ThreadClosedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadClosedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadCompactStartParams.ts b/src/runtimes/openai/generated/v2/ThreadCompactStartParams.ts deleted file mode 100644 index bdb894f..0000000 --- a/src/runtimes/openai/generated/v2/ThreadCompactStartParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadCompactStartParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadCompactStartResponse.ts b/src/runtimes/openai/generated/v2/ThreadCompactStartResponse.ts deleted file mode 100644 index 3794feb..0000000 --- a/src/runtimes/openai/generated/v2/ThreadCompactStartResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadCompactStartResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadDecrementElicitationParams.ts b/src/runtimes/openai/generated/v2/ThreadDecrementElicitationParams.ts deleted file mode 100644 index 3dcdceb..0000000 --- a/src/runtimes/openai/generated/v2/ThreadDecrementElicitationParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for `thread/decrement_elicitation`. - */ -export type ThreadDecrementElicitationParams = { - /** - * Thread whose out-of-band elicitation counter should be decremented. - */ - threadId: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadDecrementElicitationResponse.ts b/src/runtimes/openai/generated/v2/ThreadDecrementElicitationResponse.ts deleted file mode 100644 index d0f20a2..0000000 --- a/src/runtimes/openai/generated/v2/ThreadDecrementElicitationResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Response for `thread/decrement_elicitation`. - */ -export type ThreadDecrementElicitationResponse = { - /** - * Current out-of-band elicitation count after the decrement. - */ - count: bigint; - /** - * Whether timeout accounting remains paused after applying the decrement. - */ - paused: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadDeleteParams.ts b/src/runtimes/openai/generated/v2/ThreadDeleteParams.ts deleted file mode 100644 index 9f08a79..0000000 --- a/src/runtimes/openai/generated/v2/ThreadDeleteParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadDeleteParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadDeleteResponse.ts b/src/runtimes/openai/generated/v2/ThreadDeleteResponse.ts deleted file mode 100644 index 1af1c30..0000000 --- a/src/runtimes/openai/generated/v2/ThreadDeleteResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadDeleteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadDeletedNotification.ts b/src/runtimes/openai/generated/v2/ThreadDeletedNotification.ts deleted file mode 100644 index 4355ea3..0000000 --- a/src/runtimes/openai/generated/v2/ThreadDeletedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadDeletedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadExtra.ts b/src/runtimes/openai/generated/v2/ThreadExtra.ts deleted file mode 100644 index aa35e45..0000000 --- a/src/runtimes/openai/generated/v2/ThreadExtra.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Extra app-server data for a thread. - */ -export type ThreadExtra = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadForkParams.ts b/src/runtimes/openai/generated/v2/ThreadForkParams.ts deleted file mode 100644 index 534adbb..0000000 --- a/src/runtimes/openai/generated/v2/ThreadForkParams.ts +++ /dev/null @@ -1,83 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { JsonValue } from "../serde_json/JsonValue"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AskForApproval } from "./AskForApproval"; -import type { SandboxMode } from "./SandboxMode"; -import type { ThreadSource } from "./ThreadSource"; - -/** - * There are two ways to fork a thread: - * 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. - * 2. By path: load the thread from disk by path and fork it into a new thread. - * - * If using a non-empty path, the thread_id param will be ignored. - * Empty string path values are treated as absent. - * - * Prefer using thread_id whenever possible. - */ -export type ThreadForkParams = { - threadId: string; - /** - * Optional last turn id to fork through, inclusive. - * - * When specified, turns after `last_turn_id` are omitted from the fork. - * The referenced turn cannot be in progress. - */ - lastTurnId?: string | null; - /** - * Optional turn id to fork before, excluding that turn and all later turns. - * Cannot be combined with `last_turn_id`. - */ - beforeTurnId?: string | null; - /** - * [UNSTABLE] Specify the rollout path to fork from. - * If specified, the thread_id param will be ignored. - */ - path?: string | null; - /** - * Configuration overrides for the forked thread, if any. - */ - model?: string | null; - modelProvider?: string | null; - serviceTier?: string | null | null; - cwd?: string | null; - /** - * Replace the thread's runtime workspace roots. Paths must be absolute. - */ - runtimeWorkspaceRoots?: Array | null; - approvalPolicy?: AskForApproval | null; - /** - * Override where approval requests are routed for review on this thread - * and subsequent turns. - */ - approvalsReviewer?: ApprovalsReviewer | null; - sandbox?: SandboxMode | null; - /** - * Named profile id for the forked thread. Cannot be combined with - * `sandbox`. - */ - permissions?: string | null; - config?: { [key in string]?: JsonValue } | null; - baseInstructions?: string | null; - developerInstructions?: string | null; - ephemeral?: boolean; - /** - * Optional client-supplied analytics source classification for this forked thread. - */ - threadSource?: ThreadSource | null; - /** - * When true, return only thread metadata and live fork state without - * populating `thread.turns`. This is useful when the client plans to call - * `thread/turns/list` immediately after forking. - */ - excludeTurns?: boolean; - /** - * When true, carry the source thread's current goal into the fork without - * starting its initial automatic continuation. The next explicit turn owns - * the goal lifecycle, and normal automatic continuation resumes after it. - */ - deferGoalContinuation?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadForkResponse.ts b/src/runtimes/openai/generated/v2/ThreadForkResponse.ts deleted file mode 100644 index 0b93c18..0000000 --- a/src/runtimes/openai/generated/v2/ThreadForkResponse.ts +++ /dev/null @@ -1,49 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { LegacyAppPathString } from "../LegacyAppPathString"; -import type { MultiAgentMode } from "../MultiAgentMode"; -import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ActivePermissionProfile } from "./ActivePermissionProfile"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AskForApproval } from "./AskForApproval"; -import type { SandboxPolicy } from "./SandboxPolicy"; -import type { Thread } from "./Thread"; - -export type ThreadForkResponse = { - thread: Thread; - model: string; - modelProvider: string; - serviceTier: string | null; - cwd: AbsolutePathBuf; - /** - * Thread-scoped runtime workspace roots used to materialize - * `:workspace_roots`. - */ - runtimeWorkspaceRoots: Array; - /** - * Environment-native paths to instruction source files currently loaded for this thread. - */ - instructionSources: Array; - approvalPolicy: AskForApproval; - /** - * Reviewer currently used for approval requests on this thread. - */ - approvalsReviewer: ApprovalsReviewer; - /** - * Legacy sandbox policy retained for compatibility. Experimental clients - * should prefer `activePermissionProfile` for profile provenance. - */ - sandbox: SandboxPolicy; - /** - * Named or implicit built-in profile that produced the active - * permissions, when known. - */ - activePermissionProfile: ActivePermissionProfile | null; - reasoningEffort: ReasoningEffort | null; - /** - * @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. - */ - multiAgentMode: MultiAgentMode; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadGoal.ts b/src/runtimes/openai/generated/v2/ThreadGoal.ts deleted file mode 100644 index 06274ca..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoal.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoalStatus } from "./ThreadGoalStatus"; - -export type ThreadGoal = { - threadId: string; - objective: string; - status: ThreadGoalStatus; - tokenBudget: number | null; - tokensUsed: number; - timeUsedSeconds: number; - createdAt: number; - updatedAt: number; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalClearParams.ts b/src/runtimes/openai/generated/v2/ThreadGoalClearParams.ts deleted file mode 100644 index f12b4b8..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoalClearParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalClearParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalClearResponse.ts b/src/runtimes/openai/generated/v2/ThreadGoalClearResponse.ts deleted file mode 100644 index 3d17dd9..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoalClearResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalClearResponse = { cleared: boolean }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalClearedNotification.ts b/src/runtimes/openai/generated/v2/ThreadGoalClearedNotification.ts deleted file mode 100644 index ecb15e4..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoalClearedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalClearedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalGetParams.ts b/src/runtimes/openai/generated/v2/ThreadGoalGetParams.ts deleted file mode 100644 index 334444e..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoalGetParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalGetParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalGetResponse.ts b/src/runtimes/openai/generated/v2/ThreadGoalGetResponse.ts deleted file mode 100644 index b82d7a9..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoalGetResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoal } from "./ThreadGoal"; - -export type ThreadGoalGetResponse = { goal: ThreadGoal | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalSetParams.ts b/src/runtimes/openai/generated/v2/ThreadGoalSetParams.ts deleted file mode 100644 index d7d7914..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoalSetParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoalStatus } from "./ThreadGoalStatus"; - -export type ThreadGoalSetParams = { - threadId: string; - objective?: string | null; - status?: ThreadGoalStatus | null; - tokenBudget?: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalSetResponse.ts b/src/runtimes/openai/generated/v2/ThreadGoalSetResponse.ts deleted file mode 100644 index 78a25e0..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoalSetResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoal } from "./ThreadGoal"; - -export type ThreadGoalSetResponse = { goal: ThreadGoal }; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalStatus.ts b/src/runtimes/openai/generated/v2/ThreadGoalStatus.ts deleted file mode 100644 index 5a897f2..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoalStatus.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalStatus = - | "active" - | "paused" - | "blocked" - | "usageLimited" - | "budgetLimited" - | "complete"; diff --git a/src/runtimes/openai/generated/v2/ThreadGoalUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadGoalUpdatedNotification.ts deleted file mode 100644 index f4befc1..0000000 --- a/src/runtimes/openai/generated/v2/ThreadGoalUpdatedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoal } from "./ThreadGoal"; - -export type ThreadGoalUpdatedNotification = { - threadId: string; - turnId: string | null; - goal: ThreadGoal; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadIncrementElicitationParams.ts b/src/runtimes/openai/generated/v2/ThreadIncrementElicitationParams.ts deleted file mode 100644 index 12208cc..0000000 --- a/src/runtimes/openai/generated/v2/ThreadIncrementElicitationParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for `thread/increment_elicitation`. - */ -export type ThreadIncrementElicitationParams = { - /** - * Thread whose out-of-band elicitation counter should be incremented. - */ - threadId: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadIncrementElicitationResponse.ts b/src/runtimes/openai/generated/v2/ThreadIncrementElicitationResponse.ts deleted file mode 100644 index eed5c35..0000000 --- a/src/runtimes/openai/generated/v2/ThreadIncrementElicitationResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Response for `thread/increment_elicitation`. - */ -export type ThreadIncrementElicitationResponse = { - /** - * Current out-of-band elicitation count after the increment. - */ - count: bigint; - /** - * Whether timeout accounting is paused after applying the increment. - */ - paused: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadInjectItemsResponse.ts b/src/runtimes/openai/generated/v2/ThreadInjectItemsResponse.ts deleted file mode 100644 index 60dcf0d..0000000 --- a/src/runtimes/openai/generated/v2/ThreadInjectItemsResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadInjectItemsResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadItem.ts b/src/runtimes/openai/generated/v2/ThreadItem.ts deleted file mode 100644 index 34a003a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadItem.ts +++ /dev/null @@ -1,177 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ImageGenerationItem } from "../ImageGenerationItem"; -import type { LegacyAppPathString } from "../LegacyAppPathString"; -import type { MessagePhase } from "../MessagePhase"; -import type { ReasoningEffort } from "../ReasoningEffort"; -import type { SleepItem } from "../SleepItem"; -import type { WebSearchItem } from "../WebSearchItem"; -import type { JsonValue } from "../serde_json/JsonValue"; -import type { AgentMessageDelivery } from "./AgentMessageDelivery"; -import type { CollabAgentState } from "./CollabAgentState"; -import type { CollabAgentTool } from "./CollabAgentTool"; -import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; -import type { CommandAction } from "./CommandAction"; -import type { CommandExecutionSource } from "./CommandExecutionSource"; -import type { CommandExecutionStatus } from "./CommandExecutionStatus"; -import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; -import type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; -import type { FileUpdateChange } from "./FileUpdateChange"; -import type { HookPromptFragment } from "./HookPromptFragment"; -import type { McpToolCallAppContext } from "./McpToolCallAppContext"; -import type { McpToolCallError } from "./McpToolCallError"; -import type { McpToolCallResult } from "./McpToolCallResult"; -import type { McpToolCallStatus } from "./McpToolCallStatus"; -import type { MemoryCitation } from "./MemoryCitation"; -import type { PatchApplyStatus } from "./PatchApplyStatus"; -import type { SubAgentActivityKind } from "./SubAgentActivityKind"; -import type { UserInput } from "./UserInput"; - -export type ThreadItem = - | { type: "userMessage"; id: string; clientId: string | null; content: Array } - | { type: "hookPrompt"; id: string; fragments: Array } - | { - type: "agentMessage"; - id: string; - text: string; - phase: MessagePhase | null; - memoryCitation: MemoryCitation | null; - delivery: AgentMessageDelivery | null; - } - | { type: "plan"; id: string; text: string } - | { type: "reasoning"; id: string; summary: Array; content: Array } - | { - type: "commandExecution"; - id: string; - /** - * Trusted first-party plugin id when this command resolves to one plugin script. - */ - pluginId: string | null; - /** - * Safe plugin-relative path when this command resolves to one plugin script. - */ - scriptPath: string | null; - /** - * The command to be executed. - */ - command: string; - /** - * The command's working directory. - */ - cwd: LegacyAppPathString; - /** - * Identifier for the underlying PTY process (when available). - */ - processId: string | null; - source: CommandExecutionSource; - status: CommandExecutionStatus; - /** - * A best-effort parsing of the command to understand the action(s) it will perform. - * This returns a list of CommandAction objects because a single shell command may - * be composed of many commands piped together. - */ - commandActions: Array; - /** - * The command's output, aggregated from stdout and stderr. - */ - aggregatedOutput: string | null; - /** - * The command's exit code. - */ - exitCode: number | null; - /** - * The duration of the command execution in milliseconds. - */ - durationMs: number | null; - } - | { type: "fileChange"; id: string; changes: Array; status: PatchApplyStatus } - | { - type: "mcpToolCall"; - id: string; - server: string; - tool: string; - status: McpToolCallStatus; - arguments: JsonValue; - appContext: McpToolCallAppContext | null; - /** - * Deprecated: use `appContext.resourceUri` instead. - */ - mcpAppResourceUri?: string; - pluginId: string | null; - readOnlyHint: boolean | null; - result: McpToolCallResult | null; - error: McpToolCallError | null; - /** - * The duration of the MCP tool call in milliseconds. - */ - durationMs: number | null; - } - | { - type: "dynamicToolCall"; - id: string; - namespace: string | null; - tool: string; - arguments: JsonValue; - status: DynamicToolCallStatus; - contentItems: Array | null; - success: boolean | null; - /** - * The duration of the dynamic tool call in milliseconds. - */ - durationMs: number | null; - } - | { - type: "collabAgentToolCall"; - /** - * Unique identifier for this collab tool call. - */ - id: string; - /** - * Name of the collab tool that was invoked. - */ - tool: CollabAgentTool; - /** - * Current status of the collab tool call. - */ - status: CollabAgentToolCallStatus; - /** - * Thread ID of the agent issuing the collab request. - */ - senderThreadId: string; - /** - * Thread ID of the receiving agent, when applicable. In case of spawn operation, - * this corresponds to the newly spawned agent. - */ - receiverThreadIds: Array; - /** - * Prompt text sent as part of the collab tool call, when available. - */ - prompt: string | null; - /** - * Model requested for the spawned agent, when applicable. - */ - model: string | null; - /** - * Reasoning effort requested for the spawned agent, when applicable. - */ - reasoningEffort: ReasoningEffort | null; - /** - * Last known status of the target agents, when available. - */ - agentsStates: { [key in string]?: CollabAgentState }; - } - | { - type: "subAgentActivity"; - id: string; - kind: SubAgentActivityKind; - agentThreadId: string; - agentPath: string; - } - | ({ type: "webSearch" } & WebSearchItem) - | { type: "imageView"; id: string; path: LegacyAppPathString } - | ({ type: "sleep" } & SleepItem) - | ({ type: "imageGeneration" } & ImageGenerationItem) - | { type: "enteredReviewMode"; id: string; review: string } - | { type: "exitedReviewMode"; id: string; review: string } - | { type: "contextCompaction"; id: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadItemEntry.ts b/src/runtimes/openai/generated/v2/ThreadItemEntry.ts deleted file mode 100644 index d78531c..0000000 --- a/src/runtimes/openai/generated/v2/ThreadItemEntry.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadItem } from "./ThreadItem"; - -export type ThreadItemEntry = { - /** - * Turn containing this item. - */ - turnId: string; - item: ThreadItem; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadItemsListParams.ts b/src/runtimes/openai/generated/v2/ThreadItemsListParams.ts deleted file mode 100644 index e6ec19b..0000000 --- a/src/runtimes/openai/generated/v2/ThreadItemsListParams.ts +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SortDirection } from "./SortDirection"; - -export type ThreadItemsListParams = { - threadId: string; - /** - * Optional turn id to filter by. When omitted, returns items across the thread. - */ - turnId?: string | null; - /** - * Opaque cursor to pass to the next call to continue after the last item. - */ - cursor?: string | null; - /** - * Optional item page size. - */ - limit?: number | null; - /** - * Optional item pagination direction; defaults to ascending. - */ - sortDirection?: SortDirection | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadItemsListResponse.ts b/src/runtimes/openai/generated/v2/ThreadItemsListResponse.ts deleted file mode 100644 index d0804cb..0000000 --- a/src/runtimes/openai/generated/v2/ThreadItemsListResponse.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadItemEntry } from "./ThreadItemEntry"; - -export type ThreadItemsListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * if None, there are no more items to return. - */ - nextCursor: string | null; - /** - * Opaque cursor to pass as `cursor` when reversing `sortDirection`. - * This is only populated when the page contains at least one item. - */ - backwardsCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadListParams.ts b/src/runtimes/openai/generated/v2/ThreadListParams.ts deleted file mode 100644 index 0000d80..0000000 --- a/src/runtimes/openai/generated/v2/ThreadListParams.ts +++ /dev/null @@ -1,74 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SortDirection } from "./SortDirection"; -import type { ThreadSortKey } from "./ThreadSortKey"; -import type { ThreadSourceKind } from "./ThreadSourceKind"; - -export type ThreadListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a reasonable server-side value. - */ - limit?: number | null; - /** - * Optional sort key; defaults to created_at. - */ - sortKey?: ThreadSortKey | null; - /** - * Optional sort direction; defaults to descending (newest first). - */ - sortDirection?: SortDirection | null; - /** - * Optional provider filter; when set, only sessions recorded under these - * providers are returned. When present but empty, includes all providers. - */ - modelProviders?: Array | null; - /** - * Optional source filter; when set, only sessions from these source kinds - * are returned. When omitted or empty, defaults to interactive sources. - */ - sourceKinds?: Array | null; - /** - * Optional archived filter; when set to true, only archived threads are returned. - * If false or null, only non-archived threads are returned. - */ - archived?: boolean | null; - /** - * Omit to include every section, set to `null` for unsectioned threads, - * or provide a section ID to return only threads in that section. - */ - sectionId?: string | null; - /** - * Omit to include every project, set to null for unassigned threads, - * or provide a project ID to return only threads in that project. - */ - projectId?: string | null; - /** - * Optional cwd filter or filters; when set, only threads whose session cwd - * exactly matches one of these paths are returned. - */ - cwd?: string | Array | null; - /** - * If true, return from the state DB without scanning JSONL rollouts to - * repair thread metadata. Omitted or false preserves scan-and-repair - * behavior. - */ - useStateDbOnly?: boolean; - /** - * Optional substring filter for the extracted thread title. - */ - searchTerm?: string | null; - /** - * Optional direct parent thread filter. Mutually exclusive with `ancestorThreadId`. - */ - parentThreadId?: string | null; - /** - * Optional ancestor thread filter. Returns spawned descendants at any depth, excluding the - * ancestor itself. Mutually exclusive with `parentThreadId`. - */ - ancestorThreadId?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadListResponse.ts b/src/runtimes/openai/generated/v2/ThreadListResponse.ts deleted file mode 100644 index 4496da4..0000000 --- a/src/runtimes/openai/generated/v2/ThreadListResponse.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread"; - -export type ThreadListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * if None, there are no more items to return. - */ - nextCursor: string | null; - /** - * Opaque cursor to pass as `cursor` when reversing `sortDirection`. - * This is only populated when the page contains at least one thread. - * Use it with the opposite `sortDirection`; for timestamp sorts it anchors - * at the start of the page timestamp so same-second updates are not skipped. - */ - backwardsCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadLoadedListParams.ts b/src/runtimes/openai/generated/v2/ThreadLoadedListParams.ts deleted file mode 100644 index 7269540..0000000 --- a/src/runtimes/openai/generated/v2/ThreadLoadedListParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadLoadedListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to no limit. - */ - limit?: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadLoadedListResponse.ts b/src/runtimes/openai/generated/v2/ThreadLoadedListResponse.ts deleted file mode 100644 index 5555ac2..0000000 --- a/src/runtimes/openai/generated/v2/ThreadLoadedListResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadLoadedListResponse = { - /** - * Thread ids for sessions currently loaded in memory. - */ - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * if None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadMemoryModeSetParams.ts b/src/runtimes/openai/generated/v2/ThreadMemoryModeSetParams.ts deleted file mode 100644 index f4ec0a6..0000000 --- a/src/runtimes/openai/generated/v2/ThreadMemoryModeSetParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadMemoryMode } from "../ThreadMemoryMode"; - -export type ThreadMemoryModeSetParams = { threadId: string; mode: ThreadMemoryMode }; diff --git a/src/runtimes/openai/generated/v2/ThreadMemoryModeSetResponse.ts b/src/runtimes/openai/generated/v2/ThreadMemoryModeSetResponse.ts deleted file mode 100644 index 49b42fd..0000000 --- a/src/runtimes/openai/generated/v2/ThreadMemoryModeSetResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadMemoryModeSetResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadMetadataGitInfoUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadMetadataGitInfoUpdateParams.ts deleted file mode 100644 index 391427c..0000000 --- a/src/runtimes/openai/generated/v2/ThreadMetadataGitInfoUpdateParams.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadMetadataGitInfoUpdateParams = { - /** - * Omit to leave the stored commit unchanged, set to `null` to clear it, - * or provide a non-empty string to replace it. - */ - sha?: string | null; - /** - * Omit to leave the stored branch unchanged, set to `null` to clear it, - * or provide a non-empty string to replace it. - */ - branch?: string | null; - /** - * Omit to leave the stored origin URL unchanged, set to `null` to clear it, - * or provide a non-empty string to replace it. - */ - originUrl?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadMetadataUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadMetadataUpdateParams.ts deleted file mode 100644 index ad18c86..0000000 --- a/src/runtimes/openai/generated/v2/ThreadMetadataUpdateParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadMetadataGitInfoUpdateParams } from "./ThreadMetadataGitInfoUpdateParams"; - -export type ThreadMetadataUpdateParams = { - threadId: string; - /** - * Omit to leave the project unchanged, use an empty string to clear it, - * or provide an existing project ID to assign it. - */ - projectId?: string | null; - /** - * Patch the stored Git metadata for this thread. - * Omit a field to leave it unchanged, set it to `null` to clear it, or - * provide a string to replace the stored value. - */ - gitInfo?: ThreadMetadataGitInfoUpdateParams | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadMetadataUpdateResponse.ts b/src/runtimes/openai/generated/v2/ThreadMetadataUpdateResponse.ts deleted file mode 100644 index 9b1b298..0000000 --- a/src/runtimes/openai/generated/v2/ThreadMetadataUpdateResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread"; - -export type ThreadMetadataUpdateResponse = { thread: Thread }; diff --git a/src/runtimes/openai/generated/v2/ThreadNameUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadNameUpdatedNotification.ts deleted file mode 100644 index 1bee8b5..0000000 --- a/src/runtimes/openai/generated/v2/ThreadNameUpdatedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadNameUpdatedNotification = { threadId: string; threadName?: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadProjectUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadProjectUpdatedNotification.ts deleted file mode 100644 index 1d882b1..0000000 --- a/src/runtimes/openai/generated/v2/ThreadProjectUpdatedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadProjectUpdatedNotification = { threadId: string; projectId: string | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueAddParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueAddParams.ts deleted file mode 100644 index a9b03c9..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueAddParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { UserInput } from "./UserInput"; - -export type ThreadQueueAddParams = { - threadId: string; - input: Array; - clientUserMessageId: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueAddResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueAddResponse.ts deleted file mode 100644 index fd7c088..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueAddResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { QueuedSubmission } from "./QueuedSubmission"; - -export type ThreadQueueAddResponse = { queuedSubmission: QueuedSubmission }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueChangedNotification.ts b/src/runtimes/openai/generated/v2/ThreadQueueChangedNotification.ts deleted file mode 100644 index a6ff3b5..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueChangedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadQueueChangedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueDeleteParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueDeleteParams.ts deleted file mode 100644 index d0e8f3a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueDeleteParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadQueueDeleteParams = { threadId: string; queuedSubmissionId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueDeleteResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueDeleteResponse.ts deleted file mode 100644 index 9f3f48a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueDeleteResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadQueueDeleteResponse = { deleted: boolean }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueListParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueListParams.ts deleted file mode 100644 index fe23183..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueListParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadQueueListParams = { - threadId: string; - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to the standard thread-list page size. - */ - limit?: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueListResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueListResponse.ts deleted file mode 100644 index 077bf4d..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueListResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { QueuedSubmission } from "./QueuedSubmission"; - -export type ThreadQueueListResponse = { - data: Array; - /** - * Opaque cursor for the next page, or `null` when no submissions remain. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueReorderParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueReorderParams.ts deleted file mode 100644 index 86a98f1..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueReorderParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadQueueReorderParams = { threadId: string; queuedSubmissionIds: Array }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueReorderResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueReorderResponse.ts deleted file mode 100644 index 3208aa5..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueReorderResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadQueueReorderResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueStartParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueStartParams.ts deleted file mode 100644 index cf2bed1..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueStartParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadQueueStartParams = { threadId: string; queuedSubmissionId?: string | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueStartResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueStartResponse.ts deleted file mode 100644 index b0f17c8..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueStartResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn"; - -export type ThreadQueueStartResponse = { turn: Turn }; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadQueueUpdateParams.ts deleted file mode 100644 index 39ee6e7..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueUpdateParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { UserInput } from "./UserInput"; - -export type ThreadQueueUpdateParams = { - threadId: string; - queuedSubmissionId: string; - input: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadQueueUpdateResponse.ts b/src/runtimes/openai/generated/v2/ThreadQueueUpdateResponse.ts deleted file mode 100644 index ac663a6..0000000 --- a/src/runtimes/openai/generated/v2/ThreadQueueUpdateResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { QueuedSubmission } from "./QueuedSubmission"; - -export type ThreadQueueUpdateResponse = { queuedSubmission: QueuedSubmission }; diff --git a/src/runtimes/openai/generated/v2/ThreadReadParams.ts b/src/runtimes/openai/generated/v2/ThreadReadParams.ts deleted file mode 100644 index b088639..0000000 --- a/src/runtimes/openai/generated/v2/ThreadReadParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadReadParams = { - threadId: string; - /** - * When true, include turns and their items from rollout history. - */ - includeTurns?: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadReadResponse.ts b/src/runtimes/openai/generated/v2/ThreadReadResponse.ts deleted file mode 100644 index 2bbdd1a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadReadResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread"; - -export type ThreadReadResponse = { thread: Thread }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioParams.ts deleted file mode 100644 index 0d0867e..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk"; - -/** - * EXPERIMENTAL - append audio input to thread realtime. - */ -export type ThreadRealtimeAppendAudioParams = { threadId: string; audio: ThreadRealtimeAudioChunk }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioResponse.ts deleted file mode 100644 index 063e8cb..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendAudioResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - response for appending realtime audio input. - */ -export type ThreadRealtimeAppendAudioResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechParams.ts deleted file mode 100644 index 6839405..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - append speakable text to thread realtime. - */ -export type ThreadRealtimeAppendSpeechParams = { threadId: string; text: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechResponse.ts deleted file mode 100644 index 4963999..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendSpeechResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - response for appending realtime speech. - */ -export type ThreadRealtimeAppendSpeechResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextParams.ts deleted file mode 100644 index f1ce8d7..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConversationTextRole } from "../ConversationTextRole"; - -/** - * EXPERIMENTAL - append text input to thread realtime. - */ -export type ThreadRealtimeAppendTextParams = { - threadId: string; - text: string; - role: ConversationTextRole; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextResponse.ts deleted file mode 100644 index 1fb9f07..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeAppendTextResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - response for appending realtime text input. - */ -export type ThreadRealtimeAppendTextResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeAudioChunk.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeAudioChunk.ts deleted file mode 100644 index 61f4414..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeAudioChunk.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - thread realtime audio chunk. - */ -export type ThreadRealtimeAudioChunk = { - data: string; - sampleRate: number; - numChannels: number; - samplesPerChannel: number | null; - itemId: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeBemItemPresentation.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeBemItemPresentation.ts deleted file mode 100644 index f59d512..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeBemItemPresentation.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - how an existing agent item appears in a realtime conversation. - */ -export type ThreadRealtimeBemItemPresentation = - | { type: "wholeItem" } - | { type: "inlineMarkdown" } - | { type: "inlineVisualization"; index: number }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeClosedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeClosedNotification.ts deleted file mode 100644 index 7f7e837..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeClosedNotification.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - emitted when thread realtime transport closes. - */ -export type ThreadRealtimeClosedNotification = { threadId: string; reason: string | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeErrorNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeErrorNotification.ts deleted file mode 100644 index 3c1e9d4..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeErrorNotification.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - emitted when thread realtime encounters an error. - */ -export type ThreadRealtimeErrorNotification = { threadId: string; message: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeInitialItem.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeInitialItem.ts deleted file mode 100644 index c765f31..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeInitialItem.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConversationTextRole } from "../ConversationTextRole"; - -/** - * EXPERIMENTAL - role-bearing text item included when a realtime V3 session starts. - */ -export type ThreadRealtimeInitialItem = { role: ConversationTextRole; text: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItem.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItem.ts deleted file mode 100644 index 7fcc1d7..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeItem.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadRealtimeBemItemPresentation } from "./ThreadRealtimeBemItemPresentation"; -import type { ThreadRealtimeSessionOutcome } from "./ThreadRealtimeSessionOutcome"; -import type { ThreadRealtimeTranscriptRole } from "./ThreadRealtimeTranscriptRole"; - -/** - * EXPERIMENTAL - a thread-scoped realtime item in the canonical timeline. - */ -export type ThreadRealtimeItem = { id: string; realtimeSessionId: string } & ( - | { type: "realtimeSessionStarted" } - | { type: "transcriptSegment"; role: ThreadRealtimeTranscriptRole; text: string } - | { - type: "bemItemPromoted"; - turnId: string; - itemId: string; - presentation: ThreadRealtimeBemItemPresentation; - } - | { type: "realtimeSessionClosed"; outcome: ThreadRealtimeSessionOutcome } -); diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItemAddedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItemAddedNotification.ts deleted file mode 100644 index 8f2c1b1..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeItemAddedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -/** - * EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend. - */ -export type ThreadRealtimeItemAddedNotification = { threadId: string; item: JsonValue }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItemCompletedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItemCompletedNotification.ts deleted file mode 100644 index 902551a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeItemCompletedNotification.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadRealtimeItem } from "./ThreadRealtimeItem"; - -/** - * EXPERIMENTAL - a realtime timeline item published after canonical commit. - */ -export type ThreadRealtimeItemCompletedNotification = { - threadId: string; - item: ThreadRealtimeItem; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItemStartedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItemStartedNotification.ts deleted file mode 100644 index e51b63d..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeItemStartedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadRealtimeItem } from "./ThreadRealtimeItem"; - -/** - * EXPERIMENTAL - a realtime timeline item started before its content streams. - */ -export type ThreadRealtimeItemStartedNotification = { threadId: string; item: ThreadRealtimeItem }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeItemTranscriptDeltaNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeItemTranscriptDeltaNotification.ts deleted file mode 100644 index 500192a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeItemTranscriptDeltaNotification.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - text appended to an active realtime transcript item. - */ -export type ThreadRealtimeItemTranscriptDeltaNotification = { - threadId: string; - itemId: string; - delta: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesParams.ts deleted file mode 100644 index b456d89..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - list voices supported by thread realtime. - */ -export type ThreadRealtimeListVoicesParams = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesResponse.ts deleted file mode 100644 index 7e7cc30..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeListVoicesResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RealtimeVoicesList } from "../RealtimeVoicesList"; - -/** - * EXPERIMENTAL - response for listing supported realtime voices. - */ -export type ThreadRealtimeListVoicesResponse = { voices: RealtimeVoicesList }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeOutputAudioDeltaNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeOutputAudioDeltaNotification.ts deleted file mode 100644 index e4cca9d..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeOutputAudioDeltaNotification.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk"; - -/** - * EXPERIMENTAL - streamed output audio emitted by thread realtime. - */ -export type ThreadRealtimeOutputAudioDeltaNotification = { - threadId: string; - audio: ThreadRealtimeAudioChunk; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeSdpNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeSdpNotification.ts deleted file mode 100644 index 38af1bc..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeSdpNotification.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session. - */ -export type ThreadRealtimeSdpNotification = { threadId: string; sdp: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeSessionOutcome.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeSessionOutcome.ts deleted file mode 100644 index c3c73df..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeSessionOutcome.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadRealtimeSessionOutcome = "ended" | "failed"; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStartParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStartParams.ts deleted file mode 100644 index 7622c43..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeStartParams.ts +++ /dev/null @@ -1,84 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CodexResponseHandoffMode } from "../CodexResponseHandoffMode"; -import type { RealtimeConversationVersion } from "../RealtimeConversationVersion"; -import type { RealtimeOutputModality } from "../RealtimeOutputModality"; -import type { RealtimeVoice } from "../RealtimeVoice"; -import type { ThreadRealtimeInitialItem } from "./ThreadRealtimeInitialItem"; -import type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTransport"; - -/** - * EXPERIMENTAL - start a thread-scoped realtime session. - */ -export type ThreadRealtimeStartParams = { - threadId: string; - /** - * Leaves Codex response handoffs to the client's explicit append calls instead of forwarding - * them automatically. Defaults to false. - */ - clientManagedHandoffs?: boolean | null; - /** - * Controls whether a realtime V3 delegation produces an acknowledgement filler. - * Omitted values preserve the Realtime API's default behavior. - */ - delegationAckFiller?: boolean | null; - /** - * Routes any transcript tail remaining at session end through Codex. Defaults to false. - * TODO: Remove this rollout knob once transcript-tail flushing is always enabled. - */ - flushTranscriptTailOnSessionEnd?: boolean | null; - /** - * Sends automatic Codex responses as realtime conversation items instead of handoff appends. - */ - codexResponsesAsItems?: boolean | null; - /** - * Optional prefix added to automatic Codex response items when `codexResponsesAsItems` is true. - */ - codexResponseItemPrefix?: string | null; - /** - * Selects how automatic Codex responses are routed in Frameless Bidi sessions. Omitted values - * default to `thinking`. Realtime V1 and V2 ignore this setting. - */ - codexResponseHandoffMode?: CodexResponseHandoffMode | null; - /** - * Overrides BEM channel prefixes by `analysis`, `commentary`, or `final`. - * Omitted channels retain their default uppercase bracketed prefixes. - */ - codexResponseHandoffChannelPrefixes?: { [key in string]?: Array } | null; - /** - * Overrides the configured realtime model for this session only. - */ - model?: string | null; - /** - * Selects text or audio output for the realtime session. Transport and voice stay - * independent so clients can choose how they connect separately from what the model emits. - */ - outputModality: RealtimeOutputModality; - /** - * Set to false to start without Codex's startup context. Omitted or null includes it. - */ - includeStartupContext?: boolean | null; - /** - * Adds complete role-bearing text items to the initial Frameless Bidi session history. - * This is only supported by realtime V3 and is sent during session startup. Requests are - * limited to 128 items and 8,192 estimated text tokens in total. - */ - initialItems?: Array | null; - /** - * Developer instructions given to the backing Codex model when this realtime session starts. - */ - realtimeStartInstructions?: string | null; - /** - * Developer instructions given to the backing Codex model when this realtime session ends. - */ - realtimeEndInstructions?: string | null; - prompt?: string | null | null; - realtimeSessionId?: string | null; - transport?: ThreadRealtimeStartTransport | null; - /** - * Overrides the configured realtime protocol version for this session only. - */ - version?: RealtimeConversationVersion | null; - voice?: RealtimeVoice | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStartResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStartResponse.ts deleted file mode 100644 index 5625456..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeStartResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - response for starting thread realtime. - */ -export type ThreadRealtimeStartResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStartTransport.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStartTransport.ts deleted file mode 100644 index 2da052b..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeStartTransport.ts +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - transport used by thread realtime. - */ -export type ThreadRealtimeStartTransport = - | { type: "websocket" } - | { - type: "webrtc"; - /** - * SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the - * realtime events data channel. - */ - sdp: string; - } - | { - type: "existingCall"; - /** - * Identifier of a realtime call already created and negotiated by the client. - */ - callId: string; - }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStartedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStartedNotification.ts deleted file mode 100644 index 2e2ab72..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeStartedNotification.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RealtimeConversationVersion } from "../RealtimeConversationVersion"; - -/** - * EXPERIMENTAL - emitted when thread realtime startup is accepted. - */ -export type ThreadRealtimeStartedNotification = { - threadId: string; - realtimeSessionId: string | null; - version: RealtimeConversationVersion; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStopParams.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStopParams.ts deleted file mode 100644 index a74f6d8..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeStopParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - stop thread realtime. - */ -export type ThreadRealtimeStopParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeStopResponse.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeStopResponse.ts deleted file mode 100644 index c87f440..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeStopResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - response for stopping thread realtime. - */ -export type ThreadRealtimeStopResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDeltaNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDeltaNotification.ts deleted file mode 100644 index 0e6e0f4..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDeltaNotification.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - flat transcript delta emitted whenever realtime - * transcript text changes. - */ -export type ThreadRealtimeTranscriptDeltaNotification = { - threadId: string; - role: string; - /** - * Live transcript delta from the realtime event. - */ - delta: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDoneNotification.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDoneNotification.ts deleted file mode 100644 index eafdbe7..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptDoneNotification.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - final transcript text emitted when realtime completes - * a transcript part. - */ -export type ThreadRealtimeTranscriptDoneNotification = { - threadId: string; - role: string; - /** - * Final complete text for the transcript part. - */ - text: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptRole.ts b/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptRole.ts deleted file mode 100644 index d957a42..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRealtimeTranscriptRole.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadRealtimeTranscriptRole = "user" | "assistant"; diff --git a/src/runtimes/openai/generated/v2/ThreadResumeResponse.ts b/src/runtimes/openai/generated/v2/ThreadResumeResponse.ts deleted file mode 100644 index b40faca..0000000 --- a/src/runtimes/openai/generated/v2/ThreadResumeResponse.ts +++ /dev/null @@ -1,68 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { LegacyAppPathString } from "../LegacyAppPathString"; -import type { MultiAgentMode } from "../MultiAgentMode"; -import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ActivePermissionProfile } from "./ActivePermissionProfile"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AskForApproval } from "./AskForApproval"; -import type { SandboxPolicy } from "./SandboxPolicy"; -import type { Thread } from "./Thread"; -import type { TurnsPage } from "./TurnsPage"; - -export type ThreadResumeResponse = { - thread: Thread; - model: string; - modelProvider: string; - serviceTier: string | null; - cwd: AbsolutePathBuf; - /** - * Thread-scoped runtime workspace roots used to materialize - * `:workspace_roots`. - */ - runtimeWorkspaceRoots: Array; - /** - * Environment-native paths to instruction source files currently loaded for this thread. - */ - instructionSources: Array; - approvalPolicy: AskForApproval; - /** - * Reviewer currently used for approval requests on this thread. - */ - approvalsReviewer: ApprovalsReviewer; - /** - * Legacy sandbox policy retained for compatibility. Experimental clients - * should prefer `activePermissionProfile` for profile provenance. - */ - sandbox: SandboxPolicy; - /** - * Named or implicit built-in profile that produced the active - * permissions, when known. - */ - activePermissionProfile: ActivePermissionProfile | null; - reasoningEffort: ReasoningEffort | null; - /** - * @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. - */ - multiAgentMode: MultiAgentMode; - /** - * `thread/turns/list` page returned when requested by `initialTurnsPage`. - */ - initialTurnsPage: TurnsPage | null; - /** - * Opaque cursor for hydrating paginated turns backwards. - * - * Pass this as `cursor` to `thread/turns/list` with - * `sortDirection: "desc"`. The first page includes the turn identified by the cursor. - */ - turnsBackwardsCursor: string | null; - /** - * Opaque cursor for hydrating paginated items backwards. - * - * Pass this as `cursor` to `thread/items/list` with - * `sortDirection: "desc"`. The first page includes the item identified by the cursor. - */ - itemsBackwardsCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRevertParams.ts b/src/runtimes/openai/generated/v2/ThreadRevertParams.ts deleted file mode 100644 index 8bf114a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRevertParams.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Replace a paginated thread's durable history with the prefix before one turn. - * - * This only changes persisted conversation history. It does not revert local file changes. - */ -export type ThreadRevertParams = { - threadId: string; - /** - * Turn excluded from the replacement history, together with every later turn. - */ - beforeTurnId: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRevertResponse.ts b/src/runtimes/openai/generated/v2/ThreadRevertResponse.ts deleted file mode 100644 index b563369..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRevertResponse.ts +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread"; - -export type ThreadRevertResponse = { - /** - * Updated loaded thread metadata. `turns` is always empty; hydrate retained history through - * `thread/turns/list`. - */ - thread: Thread; - /** - * Opaque cursor for hydrating paginated turns backwards. - * - * Pass this as `cursor` to `thread/turns/list` with - * `sortDirection: "desc"`. The first page includes the turn identified by the cursor. - */ - turnsBackwardsCursor: string | null; - /** - * Opaque cursor for hydrating paginated items backwards. - * - * Pass this as `cursor` to `thread/items/list` with - * `sortDirection: "desc"`. The first page includes the item identified by the cursor. - */ - itemsBackwardsCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRevertedNotification.ts b/src/runtimes/openai/generated/v2/ThreadRevertedNotification.ts deleted file mode 100644 index d36a6fb..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRevertedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadRevertedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadRollbackParams.ts b/src/runtimes/openai/generated/v2/ThreadRollbackParams.ts deleted file mode 100644 index d194181..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRollbackParams.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * DEPRECATED: `thread/rollback` will be removed soon. - */ -export type ThreadRollbackParams = { - threadId: string; - /** - * The number of turns to drop from the end of the thread. Must be >= 1. - * - * This only modifies the thread's history and does not revert local file changes - * that have been made by the agent. Clients are responsible for reverting these changes. - */ - numTurns: number; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadRollbackResponse.ts b/src/runtimes/openai/generated/v2/ThreadRollbackResponse.ts deleted file mode 100644 index 82f8de4..0000000 --- a/src/runtimes/openai/generated/v2/ThreadRollbackResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread"; - -export type ThreadRollbackResponse = { - /** - * The updated thread after applying the rollback, with `turns` populated. - * - * The ThreadItems stored in each Turn are lossy since we explicitly do not - * persist all agent interactions, such as command executions. This is the same - * behavior as `thread/resume`. - */ - thread: Thread; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchOccurrence.ts b/src/runtimes/openai/generated/v2/ThreadSearchOccurrence.ts deleted file mode 100644 index 0a8c82f..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSearchOccurrence.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSearchTextRange } from "./ThreadSearchTextRange"; - -/** - * One visible message occurrence returned by [`ThreadSearchOccurrencesResponse`]. - */ -export type ThreadSearchOccurrence = { - turnId: string; - itemId: string; - snippet: string; - /** - * Match range within `snippet`, in UTF-16 code units. - */ - snippetMatchRange: ThreadSearchTextRange; - /** - * Opaque inclusive cursor accepted by `thread/turns/list` for this turn. - */ - turnCursor: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesParams.ts b/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesParams.ts deleted file mode 100644 index 4020e8b..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesParams.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for searching visible message occurrences within one paginated thread. - */ -export type ThreadSearchOccurrencesParams = { - threadId: string; - /** - * Case-insensitive literal substring to find in visible user messages and final assistant - * messages. - */ - searchTerm: string; - /** - * Opaque cursor returned by a previous call for the same thread and search term. - */ - cursor?: string | null; - /** - * Optional occurrence page size. - */ - limit?: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesResponse.ts b/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesResponse.ts deleted file mode 100644 index 93cc0ec..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSearchOccurrencesResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSearchOccurrence } from "./ThreadSearchOccurrence"; - -export type ThreadSearchOccurrencesResponse = { - /** - * Occurrences in chronological message order. - */ - data: Array; - /** - * Opaque cursor to continue after the last returned occurrence. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchParams.ts b/src/runtimes/openai/generated/v2/ThreadSearchParams.ts deleted file mode 100644 index b3b8073..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSearchParams.ts +++ /dev/null @@ -1,39 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SortDirection } from "./SortDirection"; -import type { ThreadSearchSortKey } from "./ThreadSearchSortKey"; -import type { ThreadSourceKind } from "./ThreadSourceKind"; - -export type ThreadSearchParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a reasonable server-side value. - */ - limit?: number | null; - /** - * Optional sort key; defaults to created_at. - */ - sortKey?: ThreadSearchSortKey | null; - /** - * Optional sort direction; defaults to descending (newest first). - */ - sortDirection?: SortDirection | null; - /** - * Optional source filter; when set, only sessions from these source kinds - * are returned. When omitted or empty, defaults to interactive sources. - */ - sourceKinds?: Array | null; - /** - * Optional archived filter; when set to true, only archived threads are returned. - * If false or null, only non-archived threads are returned. - */ - archived?: boolean | null; - /** - * Required substring/full-text query for thread search. - */ - searchTerm: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchResponse.ts b/src/runtimes/openai/generated/v2/ThreadSearchResponse.ts deleted file mode 100644 index 5353aad..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSearchResponse.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSearchResult } from "./ThreadSearchResult"; - -export type ThreadSearchResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * if None, there are no more items to return. - */ - nextCursor: string | null; - /** - * Opaque cursor to pass as `cursor` when reversing `sortDirection`. - * This is only populated when the page contains at least one thread. - * Use it with the opposite `sortDirection`; for timestamp sorts it anchors - * at the start of the page timestamp so same-second updates are not skipped. - */ - backwardsCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchResult.ts b/src/runtimes/openai/generated/v2/ThreadSearchResult.ts deleted file mode 100644 index 84efaa0..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSearchResult.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread"; - -export type ThreadSearchResult = { thread: Thread; snippet: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchSortKey.ts b/src/runtimes/openai/generated/v2/ThreadSearchSortKey.ts deleted file mode 100644 index 4abf271..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSearchSortKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSearchSortKey = "created_at" | "updated_at" | "recency_at"; diff --git a/src/runtimes/openai/generated/v2/ThreadSearchTextRange.ts b/src/runtimes/openai/generated/v2/ThreadSearchTextRange.ts deleted file mode 100644 index 0545e13..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSearchTextRange.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * UTF-16 code-unit range within `snippet`. - */ -export type ThreadSearchTextRange = { - /** - * Inclusive UTF-16 code-unit offset. - */ - start: number; - /** - * Exclusive UTF-16 code-unit offset. - */ - end: number; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSection.ts b/src/runtimes/openai/generated/v2/ThreadSection.ts deleted file mode 100644 index 77121cf..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSection.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; - -/** - * An independently persisted, user-visible thread section. - */ -export type ThreadSection = { - /** - * Opaque UUIDv7 identity that remains stable when the section is renamed. - */ - id: string; - /** - * The current user-visible section name. - */ - name: string; - /** - * Optional appearance synchronized across clients. - */ - appearance: ThreadSectionAppearance | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionAppearance.ts b/src/runtimes/openai/generated/v2/ThreadSectionAppearance.ts deleted file mode 100644 index f99774c..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionAppearance.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Extensible visual presentation for a custom thread section. - */ -export type ThreadSectionAppearance = { icon: string | null; color: string | null }; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionCreateParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionCreateParams.ts deleted file mode 100644 index 929fd5a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionCreateParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; - -/** - * Parameters for creating an independently persisted thread section. - */ -export type ThreadSectionCreateParams = { - /** - * The user-visible name of the section. - */ - name: string; - appearance?: ThreadSectionAppearance | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionCreateResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionCreateResponse.ts deleted file mode 100644 index 8a1b362..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionCreateResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSection } from "./ThreadSection"; - -/** - * The independently persisted section created by the server. - */ -export type ThreadSectionCreateResponse = { section: ThreadSection }; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionDeleteParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionDeleteParams.ts deleted file mode 100644 index 425eb48..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionDeleteParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for deleting an independently persisted thread section. - */ -export type ThreadSectionDeleteParams = { - /** - * The stable, server-generated identity of the section to delete. - */ - sectionId: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionDeleteResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionDeleteResponse.ts deleted file mode 100644 index 0ad58d0..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionDeleteResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful deletion does not return additional section data. - */ -export type ThreadSectionDeleteResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionListParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionListParams.ts deleted file mode 100644 index abf8bd3..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionListParams.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for listing independently persisted thread sections. - */ -export type ThreadSectionListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Maximum number of sections to return. - */ - limit?: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionListResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionListResponse.ts deleted file mode 100644 index b3723a7..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionListResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSection } from "./ThreadSection"; - -/** - * One page of independently persisted thread sections. - */ -export type ThreadSectionListResponse = { - data: Array; - /** - * Opaque cursor for the next page, or `null` when no sections remain. - */ - nextCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionMoveParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionMoveParams.ts deleted file mode 100644 index b8795f3..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionMoveParams.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for moving a thread within a server-owned section ordering. - */ -export type ThreadSectionMoveParams = { - /** - * Thread to move into, within, or out of a section. - */ - threadId: string; - /** - * Destination section, or `null` to remove the thread from its section. - */ - sectionId: string | null; - /** - * Existing thread to insert before; omission or null appends to the section. - */ - beforeThreadId?: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionMoveResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionMoveResponse.ts deleted file mode 100644 index e9e0f43..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionMoveResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSectionMoveResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadSectionUpdateParams.ts deleted file mode 100644 index c7c354a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionUpdateParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; - -/** - * Parameters for updating an independently persisted thread section. - */ -export type ThreadSectionUpdateParams = { - /** - * The stable, server-generated identity of the section to update. - */ - sectionId: string; - /** - * The updated user-visible name of the section. - */ - name: string; - /** - * Omit to preserve appearance, use `null` to clear it, or provide a replacement. - */ - appearance?: ThreadSectionAppearance | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSectionUpdateResponse.ts b/src/runtimes/openai/generated/v2/ThreadSectionUpdateResponse.ts deleted file mode 100644 index aef9241..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSectionUpdateResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSection } from "./ThreadSection"; - -/** - * The independently persisted section after its name is updated. - */ -export type ThreadSectionUpdateResponse = { section: ThreadSection }; diff --git a/src/runtimes/openai/generated/v2/ThreadSetNameParams.ts b/src/runtimes/openai/generated/v2/ThreadSetNameParams.ts deleted file mode 100644 index 17d12ee..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSetNameParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSetNameParams = { threadId: string; name: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadSetNameResponse.ts b/src/runtimes/openai/generated/v2/ThreadSetNameResponse.ts deleted file mode 100644 index 09143d2..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSetNameResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSetNameResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSettings.ts b/src/runtimes/openai/generated/v2/ThreadSettings.ts deleted file mode 100644 index b2c2863..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSettings.ts +++ /dev/null @@ -1,32 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { CollaborationMode } from "../CollaborationMode"; -import type { MultiAgentMode } from "../MultiAgentMode"; -import type { Personality } from "../Personality"; -import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ReasoningSummary } from "../ReasoningSummary"; -import type { ActivePermissionProfile } from "./ActivePermissionProfile"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AskForApproval } from "./AskForApproval"; -import type { SandboxPolicy } from "./SandboxPolicy"; - -export type ThreadSettings = { - cwd: AbsolutePathBuf; - approvalPolicy: AskForApproval; - approvalsReviewer: ApprovalsReviewer; - sandboxPolicy: SandboxPolicy; - activePermissionProfile: ActivePermissionProfile | null; - model: string; - modelProvider: string; - serviceTier: string | null; - effort: ReasoningEffort | null; - summary: ReasoningSummary | null; - collaborationMode: CollaborationMode; - /** - * @deprecated Always `explicitRequestOnly`. Use `effort` for Ultra behavior. - */ - multiAgentMode: MultiAgentMode; - personality: Personality | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSettingsUpdateParams.ts b/src/runtimes/openai/generated/v2/ThreadSettingsUpdateParams.ts deleted file mode 100644 index 1b64fa1..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSettingsUpdateParams.ts +++ /dev/null @@ -1,68 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CollaborationMode } from "../CollaborationMode"; -import type { MultiAgentMode } from "../MultiAgentMode"; -import type { Personality } from "../Personality"; -import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ReasoningSummary } from "../ReasoningSummary"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AskForApproval } from "./AskForApproval"; -import type { SandboxPolicy } from "./SandboxPolicy"; - -export type ThreadSettingsUpdateParams = { - threadId: string; - /** - * Override the working directory for subsequent turns. - */ - cwd?: string | null; - /** - * Override the approval policy for subsequent turns. - */ - approvalPolicy?: AskForApproval | null; - /** - * Override where approval requests are routed for subsequent turns. - */ - approvalsReviewer?: ApprovalsReviewer | null; - /** - * Override the sandbox policy for subsequent turns. - */ - sandboxPolicy?: SandboxPolicy | null; - /** - * Select a named permissions profile id for subsequent turns. Cannot be - * combined with `sandboxPolicy`. - */ - permissions?: string | null; - /** - * Override the model for subsequent turns. - */ - model?: string | null; - /** - * Override the service tier for subsequent turns. `null` clears the - * current service tier; omission leaves it unchanged. - */ - serviceTier?: string | null | null; - /** - * Override the reasoning effort for subsequent turns. - */ - effort?: ReasoningEffort | null; - /** - * Override the reasoning summary for subsequent turns. - */ - summary?: ReasoningSummary | null; - /** - * EXPERIMENTAL - Set a pre-set collaboration mode for subsequent turns. - * - * For `collaboration_mode.settings.developer_instructions`, `null` means - * "use the built-in instructions for the selected mode". - */ - collaborationMode?: CollaborationMode | null; - /** - * @deprecated Ignored. Use `effort: "ultra"` for proactive multi-agent behavior. - */ - multiAgentMode?: MultiAgentMode | null; - /** - * Override the personality for subsequent turns. - */ - personality?: Personality | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadSettingsUpdateResponse.ts b/src/runtimes/openai/generated/v2/ThreadSettingsUpdateResponse.ts deleted file mode 100644 index 06afb97..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSettingsUpdateResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSettingsUpdateResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSettingsUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadSettingsUpdatedNotification.ts deleted file mode 100644 index fc911c1..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSettingsUpdatedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadSettings } from "./ThreadSettings"; - -export type ThreadSettingsUpdatedNotification = { - threadId: string; - threadSettings: ThreadSettings; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadShellCommandParams.ts b/src/runtimes/openai/generated/v2/ThreadShellCommandParams.ts deleted file mode 100644 index 7e7b118..0000000 --- a/src/runtimes/openai/generated/v2/ThreadShellCommandParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadShellCommandParams = { - threadId: string; - /** - * Shell command string evaluated by the thread's configured shell. - * Unlike `command/exec`, this intentionally preserves shell syntax - * such as pipes, redirects, and quoting. This runs unsandboxed with full - * access rather than inheriting the thread sandbox policy. - */ - command: string; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadShellCommandResponse.ts b/src/runtimes/openai/generated/v2/ThreadShellCommandResponse.ts deleted file mode 100644 index 9c54b45..0000000 --- a/src/runtimes/openai/generated/v2/ThreadShellCommandResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadShellCommandResponse = Record; diff --git a/src/runtimes/openai/generated/v2/ThreadSortKey.ts b/src/runtimes/openai/generated/v2/ThreadSortKey.ts deleted file mode 100644 index 21eae4e..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSortKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSortKey = "created_at" | "updated_at" | "recency_at" | "section_position"; diff --git a/src/runtimes/openai/generated/v2/ThreadSourceKind.ts b/src/runtimes/openai/generated/v2/ThreadSourceKind.ts deleted file mode 100644 index 39b1887..0000000 --- a/src/runtimes/openai/generated/v2/ThreadSourceKind.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSourceKind = - | "cli" - | "vscode" - | "exec" - | "appServer" - | "subAgent" - | "subAgentReview" - | "subAgentCompact" - | "subAgentThreadSpawn" - | "subAgentOther" - | "unknown"; diff --git a/src/runtimes/openai/generated/v2/ThreadStartResponse.ts b/src/runtimes/openai/generated/v2/ThreadStartResponse.ts deleted file mode 100644 index 0b616e2..0000000 --- a/src/runtimes/openai/generated/v2/ThreadStartResponse.ts +++ /dev/null @@ -1,49 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { LegacyAppPathString } from "../LegacyAppPathString"; -import type { MultiAgentMode } from "../MultiAgentMode"; -import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ActivePermissionProfile } from "./ActivePermissionProfile"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer"; -import type { AskForApproval } from "./AskForApproval"; -import type { SandboxPolicy } from "./SandboxPolicy"; -import type { Thread } from "./Thread"; - -export type ThreadStartResponse = { - thread: Thread; - model: string; - modelProvider: string; - serviceTier: string | null; - cwd: AbsolutePathBuf; - /** - * Thread-scoped runtime workspace roots used to materialize - * `:workspace_roots`. - */ - runtimeWorkspaceRoots: Array; - /** - * Environment-native paths to instruction source files currently loaded for this thread. - */ - instructionSources: Array; - approvalPolicy: AskForApproval; - /** - * Reviewer currently used for approval requests on this thread. - */ - approvalsReviewer: ApprovalsReviewer; - /** - * Legacy sandbox policy retained for compatibility. Experimental clients - * should prefer `activePermissionProfile` for profile provenance. - */ - sandbox: SandboxPolicy; - /** - * Named or implicit built-in profile that produced the active - * permissions, when known. - */ - activePermissionProfile: ActivePermissionProfile | null; - reasoningEffort: ReasoningEffort | null; - /** - * @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. - */ - multiAgentMode: MultiAgentMode; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadStartedNotification.ts b/src/runtimes/openai/generated/v2/ThreadStartedNotification.ts deleted file mode 100644 index 82fce9e..0000000 --- a/src/runtimes/openai/generated/v2/ThreadStartedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread"; - -export type ThreadStartedNotification = { thread: Thread }; diff --git a/src/runtimes/openai/generated/v2/ThreadStatus.ts b/src/runtimes/openai/generated/v2/ThreadStatus.ts deleted file mode 100644 index 64059b6..0000000 --- a/src/runtimes/openai/generated/v2/ThreadStatus.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadActiveFlag } from "./ThreadActiveFlag"; - -export type ThreadStatus = - | { type: "notLoaded" } - | { type: "idle" } - | { type: "systemError" } - | { type: "active"; activeFlags: Array }; diff --git a/src/runtimes/openai/generated/v2/ThreadStatusChangedNotification.ts b/src/runtimes/openai/generated/v2/ThreadStatusChangedNotification.ts deleted file mode 100644 index 3fc2e9a..0000000 --- a/src/runtimes/openai/generated/v2/ThreadStatusChangedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadStatus } from "./ThreadStatus"; - -export type ThreadStatusChangedNotification = { threadId: string; status: ThreadStatus }; diff --git a/src/runtimes/openai/generated/v2/ThreadTimelineEntry.ts b/src/runtimes/openai/generated/v2/ThreadTimelineEntry.ts deleted file mode 100644 index 27d2915..0000000 --- a/src/runtimes/openai/generated/v2/ThreadTimelineEntry.ts +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadItem } from "./ThreadItem"; -import type { ThreadRealtimeItem } from "./ThreadRealtimeItem"; -import type { TurnError } from "./TurnError"; -import type { TurnStatus } from "./TurnStatus"; - -/** - * EXPERIMENTAL - one item or turn boundary in canonical rollout order. - */ -export type ThreadTimelineEntry = - | { type: "item"; position: number; turnId: string; item: ThreadItem } - | { type: "realtime"; position: number; item: ThreadRealtimeItem } - | { type: "turnStarted"; position: number; turnId: string; startedAt: number | null } - | { - type: "turnCompleted"; - position: number; - turnId: string; - status: TurnStatus; - error: TurnError | null; - startedAt: number | null; - completedAt: number | null; - durationMs: number | null; - }; diff --git a/src/runtimes/openai/generated/v2/ThreadTimelineListParams.ts b/src/runtimes/openai/generated/v2/ThreadTimelineListParams.ts deleted file mode 100644 index 0d1d2fe..0000000 --- a/src/runtimes/openai/generated/v2/ThreadTimelineListParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - list ordinary and realtime thread history in rollout order. - */ -export type ThreadTimelineListParams = { - threadId: string; - cursor?: string | null; - limit?: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadTimelineListResponse.ts b/src/runtimes/openai/generated/v2/ThreadTimelineListResponse.ts deleted file mode 100644 index adc0195..0000000 --- a/src/runtimes/openai/generated/v2/ThreadTimelineListResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadTimelineEntry } from "./ThreadTimelineEntry"; - -/** - * EXPERIMENTAL - a bounded timeline page with its resolved opening voice state. - */ -export type ThreadTimelineListResponse = { - data: Array; - nextCursor: string | null; - activeRealtimeSessionAtPageStart: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadTokenUsage.ts b/src/runtimes/openai/generated/v2/ThreadTokenUsage.ts deleted file mode 100644 index 525619b..0000000 --- a/src/runtimes/openai/generated/v2/ThreadTokenUsage.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; - -export type ThreadTokenUsage = { - total: TokenUsageBreakdown; - last: TokenUsageBreakdown; - modelContextWindow: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadTokenUsageUpdatedNotification.ts b/src/runtimes/openai/generated/v2/ThreadTokenUsageUpdatedNotification.ts deleted file mode 100644 index 59ce27f..0000000 --- a/src/runtimes/openai/generated/v2/ThreadTokenUsageUpdatedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadTokenUsage } from "./ThreadTokenUsage"; - -export type ThreadTokenUsageUpdatedNotification = { - threadId: string; - turnId: string; - tokenUsage: ThreadTokenUsage; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadTurnsListParams.ts b/src/runtimes/openai/generated/v2/ThreadTurnsListParams.ts deleted file mode 100644 index 77b8163..0000000 --- a/src/runtimes/openai/generated/v2/ThreadTurnsListParams.ts +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SortDirection } from "./SortDirection"; -import type { TurnItemsView } from "./TurnItemsView"; - -export type ThreadTurnsListParams = { - threadId: string; - /** - * Opaque cursor to pass to the next call to continue after the last turn. - */ - cursor?: string | null; - /** - * Optional turn page size. - */ - limit?: number | null; - /** - * Optional turn pagination direction; defaults to descending. - */ - sortDirection?: SortDirection | null; - /** - * How much item detail to include for each returned turn; defaults to summary. - */ - itemsView?: TurnItemsView | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadTurnsListResponse.ts b/src/runtimes/openai/generated/v2/ThreadTurnsListResponse.ts deleted file mode 100644 index 9c961dd..0000000 --- a/src/runtimes/openai/generated/v2/ThreadTurnsListResponse.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn"; - -export type ThreadTurnsListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last turn. - * if None, there are no more turns to return. - */ - nextCursor: string | null; - /** - * Opaque cursor to pass as `cursor` when reversing `sortDirection`. - * This is only populated when the page contains at least one turn. - * Use it with the opposite `sortDirection` to include the anchor turn again - * and catch updates to that turn. - */ - backwardsCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadUnarchiveParams.ts b/src/runtimes/openai/generated/v2/ThreadUnarchiveParams.ts deleted file mode 100644 index 9b01abc..0000000 --- a/src/runtimes/openai/generated/v2/ThreadUnarchiveParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadUnarchiveParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnarchiveResponse.ts b/src/runtimes/openai/generated/v2/ThreadUnarchiveResponse.ts deleted file mode 100644 index a469265..0000000 --- a/src/runtimes/openai/generated/v2/ThreadUnarchiveResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread"; - -export type ThreadUnarchiveResponse = { thread: Thread }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnarchivedNotification.ts b/src/runtimes/openai/generated/v2/ThreadUnarchivedNotification.ts deleted file mode 100644 index 34a712e..0000000 --- a/src/runtimes/openai/generated/v2/ThreadUnarchivedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadUnarchivedNotification = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnsubscribeParams.ts b/src/runtimes/openai/generated/v2/ThreadUnsubscribeParams.ts deleted file mode 100644 index 0fd5aae..0000000 --- a/src/runtimes/openai/generated/v2/ThreadUnsubscribeParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadUnsubscribeParams = { threadId: string }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnsubscribeResponse.ts b/src/runtimes/openai/generated/v2/ThreadUnsubscribeResponse.ts deleted file mode 100644 index 10db583..0000000 --- a/src/runtimes/openai/generated/v2/ThreadUnsubscribeResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus"; - -export type ThreadUnsubscribeResponse = { status: ThreadUnsubscribeStatus }; diff --git a/src/runtimes/openai/generated/v2/ThreadUnsubscribeStatus.ts b/src/runtimes/openai/generated/v2/ThreadUnsubscribeStatus.ts deleted file mode 100644 index 2970598..0000000 --- a/src/runtimes/openai/generated/v2/ThreadUnsubscribeStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadUnsubscribeStatus = "notLoaded" | "notSubscribed" | "unsubscribed"; diff --git a/src/runtimes/openai/generated/v2/ThreadUsage.ts b/src/runtimes/openai/generated/v2/ThreadUsage.ts deleted file mode 100644 index 14a00ca..0000000 --- a/src/runtimes/openai/generated/v2/ThreadUsage.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadUsageBreakdownGroup } from "./ThreadUsageBreakdownGroup"; - -export type ThreadUsage = { - threadId: string; - estimatedUsageCreditsMicros: bigint; - estimatedUsageUsdMicros: bigint | null; - groups: Array; -}; diff --git a/src/runtimes/openai/generated/v2/ThreadUsageBreakdownGroup.ts b/src/runtimes/openai/generated/v2/ThreadUsageBreakdownGroup.ts deleted file mode 100644 index 886ee8b..0000000 --- a/src/runtimes/openai/generated/v2/ThreadUsageBreakdownGroup.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadUsageBreakdownGroup = { - model: string | null; - reasoningEffort: string | null; - speed: string | null; - estimatedUsageCreditsMicros: bigint; - netNewInputTokens: bigint | null; - cachedInputTokens: bigint | null; - inputTokens: bigint | null; - outputTokens: bigint | null; - totalTokens: bigint | null; -}; diff --git a/src/runtimes/openai/generated/v2/TokenUsageBreakdown.ts b/src/runtimes/openai/generated/v2/TokenUsageBreakdown.ts deleted file mode 100644 index 4837e56..0000000 --- a/src/runtimes/openai/generated/v2/TokenUsageBreakdown.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TokenUsageBreakdown = { - totalTokens: number; - inputTokens: number; - cachedInputTokens: number; - cacheWriteInputTokens: number; - outputTokens: number; - reasoningOutputTokens: number; -}; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputAnswer.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputAnswer.ts deleted file mode 100644 index 87e581b..0000000 --- a/src/runtimes/openai/generated/v2/ToolRequestUserInputAnswer.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL. Captures a user's answer to a request_user_input question. - */ -export type ToolRequestUserInputAnswer = { answers: Array }; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputOption.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputOption.ts deleted file mode 100644 index 6a92bc0..0000000 --- a/src/runtimes/openai/generated/v2/ToolRequestUserInputOption.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL. Defines a single selectable option for request_user_input. - */ -export type ToolRequestUserInputOption = { label: string; description: string }; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputParams.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputParams.ts deleted file mode 100644 index bb5d593..0000000 --- a/src/runtimes/openai/generated/v2/ToolRequestUserInputParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion"; - -/** - * EXPERIMENTAL. Params sent with a request_user_input event. - */ -export type ToolRequestUserInputParams = { - threadId: string; - turnId: string; - itemId: string; - questions: Array; - isBlocking: boolean; - /** - * @deprecated Use `isBlocking` to decide whether the request should block. - */ - autoResolutionMs: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputQuestion.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputQuestion.ts deleted file mode 100644 index cc1d12d..0000000 --- a/src/runtimes/openai/generated/v2/ToolRequestUserInputQuestion.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption"; - -/** - * EXPERIMENTAL. Represents one request_user_input question and its required options. - */ -export type ToolRequestUserInputQuestion = { - id: string; - header: string; - question: string; - isOther: boolean; - isSecret: boolean; - options: Array | null; -}; diff --git a/src/runtimes/openai/generated/v2/ToolRequestUserInputResponse.ts b/src/runtimes/openai/generated/v2/ToolRequestUserInputResponse.ts deleted file mode 100644 index 6005100..0000000 --- a/src/runtimes/openai/generated/v2/ToolRequestUserInputResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer"; - -/** - * EXPERIMENTAL. Response payload mapping question ids to answers. - */ -export type ToolRequestUserInputResponse = { - answers: { [key in string]?: ToolRequestUserInputAnswer }; -}; diff --git a/src/runtimes/openai/generated/v2/ToolsV2.ts b/src/runtimes/openai/generated/v2/ToolsV2.ts deleted file mode 100644 index 5a563e3..0000000 --- a/src/runtimes/openai/generated/v2/ToolsV2.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WebSearchToolConfig } from "../WebSearchToolConfig"; - -export type ToolsV2 = { web_search: WebSearchToolConfig | null }; diff --git a/src/runtimes/openai/generated/v2/Turn.ts b/src/runtimes/openai/generated/v2/Turn.ts deleted file mode 100644 index 92501a8..0000000 --- a/src/runtimes/openai/generated/v2/Turn.ts +++ /dev/null @@ -1,39 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadItem } from "./ThreadItem"; -import type { TurnError } from "./TurnError"; -import type { TurnItemsView } from "./TurnItemsView"; -import type { TurnStatus } from "./TurnStatus"; - -export type Turn = { - /** - * Identifier for this turn. Codex-generated turn IDs are UUIDv7. - */ - id: string; - /** - * Thread items currently included in this turn payload. - */ - items: Array; - /** - * Describes how much of `items` has been loaded for this turn. - */ - itemsView: TurnItemsView; - status: TurnStatus; - /** - * Only populated when the Turn's status is failed. - */ - error: TurnError | null; - /** - * Unix timestamp (in seconds) when the turn started. - */ - startedAt: number | null; - /** - * Unix timestamp (in seconds) when the turn completed. - */ - completedAt: number | null; - /** - * Duration between turn start and completion in milliseconds, if known. - */ - durationMs: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/TurnCompletedNotification.ts b/src/runtimes/openai/generated/v2/TurnCompletedNotification.ts deleted file mode 100644 index 5b585e0..0000000 --- a/src/runtimes/openai/generated/v2/TurnCompletedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn"; - -export type TurnCompletedNotification = { threadId: string; turn: Turn }; diff --git a/src/runtimes/openai/generated/v2/TurnDiffUpdatedNotification.ts b/src/runtimes/openai/generated/v2/TurnDiffUpdatedNotification.ts deleted file mode 100644 index 0f4bf14..0000000 --- a/src/runtimes/openai/generated/v2/TurnDiffUpdatedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Notification that the turn-level unified diff has changed. - * Contains the latest aggregated diff across all file changes in the turn. - */ -export type TurnDiffUpdatedNotification = { threadId: string; turnId: string; diff: string }; diff --git a/src/runtimes/openai/generated/v2/TurnError.ts b/src/runtimes/openai/generated/v2/TurnError.ts deleted file mode 100644 index 8b5aee1..0000000 --- a/src/runtimes/openai/generated/v2/TurnError.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CodexErrorInfo } from "./CodexErrorInfo"; - -export type TurnError = { - message: string; - codexErrorInfo: CodexErrorInfo | null; - additionalDetails: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/TurnInterruptParams.ts b/src/runtimes/openai/generated/v2/TurnInterruptParams.ts deleted file mode 100644 index 1373415..0000000 --- a/src/runtimes/openai/generated/v2/TurnInterruptParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TurnInterruptParams = { threadId: string; turnId: string }; diff --git a/src/runtimes/openai/generated/v2/TurnInterruptResponse.ts b/src/runtimes/openai/generated/v2/TurnInterruptResponse.ts deleted file mode 100644 index 7ce6e35..0000000 --- a/src/runtimes/openai/generated/v2/TurnInterruptResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TurnInterruptResponse = Record; diff --git a/src/runtimes/openai/generated/v2/TurnModerationMetadataNotification.ts b/src/runtimes/openai/generated/v2/TurnModerationMetadataNotification.ts deleted file mode 100644 index 0d9f07e..0000000 --- a/src/runtimes/openai/generated/v2/TurnModerationMetadataNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue"; - -export type TurnModerationMetadataNotification = { - threadId: string; - turnId: string; - metadata: JsonValue; -}; diff --git a/src/runtimes/openai/generated/v2/TurnPlanStep.ts b/src/runtimes/openai/generated/v2/TurnPlanStep.ts deleted file mode 100644 index 7e3c020..0000000 --- a/src/runtimes/openai/generated/v2/TurnPlanStep.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TurnPlanStepStatus } from "./TurnPlanStepStatus"; - -export type TurnPlanStep = { step: string; status: TurnPlanStepStatus }; diff --git a/src/runtimes/openai/generated/v2/TurnPlanStepStatus.ts b/src/runtimes/openai/generated/v2/TurnPlanStepStatus.ts deleted file mode 100644 index f6733a6..0000000 --- a/src/runtimes/openai/generated/v2/TurnPlanStepStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TurnPlanStepStatus = "pending" | "inProgress" | "completed"; diff --git a/src/runtimes/openai/generated/v2/TurnPlanUpdatedNotification.ts b/src/runtimes/openai/generated/v2/TurnPlanUpdatedNotification.ts deleted file mode 100644 index 6e48f19..0000000 --- a/src/runtimes/openai/generated/v2/TurnPlanUpdatedNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TurnPlanStep } from "./TurnPlanStep"; - -export type TurnPlanUpdatedNotification = { - threadId: string; - turnId: string; - explanation: string | null; - plan: Array; -}; diff --git a/src/runtimes/openai/generated/v2/TurnStartResponse.ts b/src/runtimes/openai/generated/v2/TurnStartResponse.ts deleted file mode 100644 index d742f41..0000000 --- a/src/runtimes/openai/generated/v2/TurnStartResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn"; - -export type TurnStartResponse = { turn: Turn }; diff --git a/src/runtimes/openai/generated/v2/TurnStartedNotification.ts b/src/runtimes/openai/generated/v2/TurnStartedNotification.ts deleted file mode 100644 index 0151127..0000000 --- a/src/runtimes/openai/generated/v2/TurnStartedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn"; - -export type TurnStartedNotification = { threadId: string; turn: Turn }; diff --git a/src/runtimes/openai/generated/v2/TurnSteerParams.ts b/src/runtimes/openai/generated/v2/TurnSteerParams.ts deleted file mode 100644 index fddb58c..0000000 --- a/src/runtimes/openai/generated/v2/TurnSteerParams.ts +++ /dev/null @@ -1,30 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AdditionalContextEntry } from "./AdditionalContextEntry"; -import type { UserInput } from "./UserInput"; - -export type TurnSteerParams = { - threadId: string; - clientUserMessageId?: string | null; - input: Array; - /** - * Optional metadata to enrich Codex's ResponsesAPI turn metadata. - * - * Entries are flattened into the JSON string sent as - * `client_metadata["x-codex-turn-metadata"]` on ResponsesAPI HTTP and websocket requests. - * - * They are not sent as top-level ResponsesAPI `client_metadata` keys, and reserved keys - * such as `session_id`, `thread_id`, `turn_id`, and `window_id` cannot be overridden. - */ - responsesapiClientMetadata?: { [key in string]?: string } | null; - /** - * Optional client-provided context fragments keyed by an opaque source identifier. - */ - additionalContext?: { [key in string]?: AdditionalContextEntry } | null; - /** - * Required active turn id precondition. The request fails when it does not - * match the currently active turn. - */ - expectedTurnId: string; -}; diff --git a/src/runtimes/openai/generated/v2/TurnSteerResponse.ts b/src/runtimes/openai/generated/v2/TurnSteerResponse.ts deleted file mode 100644 index 1ed155c..0000000 --- a/src/runtimes/openai/generated/v2/TurnSteerResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TurnSteerResponse = { turnId: string }; diff --git a/src/runtimes/openai/generated/v2/TurnsPage.ts b/src/runtimes/openai/generated/v2/TurnsPage.ts deleted file mode 100644 index 978c3fa..0000000 --- a/src/runtimes/openai/generated/v2/TurnsPage.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn"; - -export type TurnsPage = { - data: Array; - nextCursor: string | null; - backwardsCursor: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/WarningNotification.ts b/src/runtimes/openai/generated/v2/WarningNotification.ts deleted file mode 100644 index dc30ca0..0000000 --- a/src/runtimes/openai/generated/v2/WarningNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WarningNotification = { - /** - * Optional thread target when the warning applies to a specific thread. - */ - threadId: string | null; - /** - * Concise warning message for the user. - */ - message: string; -}; diff --git a/src/runtimes/openai/generated/v2/WebSearchAction.ts b/src/runtimes/openai/generated/v2/WebSearchAction.ts deleted file mode 100644 index 20801fb..0000000 --- a/src/runtimes/openai/generated/v2/WebSearchAction.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSearchAction = - | { type: "search"; query: string | null; queries: Array | null } - | { type: "openPage"; url: string | null } - | { type: "findInPage"; url: string | null; pattern: string | null } - | { type: "other" }; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxReadiness.ts b/src/runtimes/openai/generated/v2/WindowsSandboxReadiness.ts deleted file mode 100644 index 41b1161..0000000 --- a/src/runtimes/openai/generated/v2/WindowsSandboxReadiness.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WindowsSandboxReadiness = "ready" | "notConfigured" | "updateRequired"; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxReadinessResponse.ts b/src/runtimes/openai/generated/v2/WindowsSandboxReadinessResponse.ts deleted file mode 100644 index 31e0d63..0000000 --- a/src/runtimes/openai/generated/v2/WindowsSandboxReadinessResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WindowsSandboxReadiness } from "./WindowsSandboxReadiness"; - -export type WindowsSandboxReadinessResponse = { status: WindowsSandboxReadiness }; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxSetupCompletedNotification.ts b/src/runtimes/openai/generated/v2/WindowsSandboxSetupCompletedNotification.ts deleted file mode 100644 index 374f381..0000000 --- a/src/runtimes/openai/generated/v2/WindowsSandboxSetupCompletedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; - -export type WindowsSandboxSetupCompletedNotification = { - mode: WindowsSandboxSetupMode; - success: boolean; - error: string | null; -}; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxSetupMode.ts b/src/runtimes/openai/generated/v2/WindowsSandboxSetupMode.ts deleted file mode 100644 index a74bea4..0000000 --- a/src/runtimes/openai/generated/v2/WindowsSandboxSetupMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WindowsSandboxSetupMode = "elevated" | "unelevated"; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartParams.ts b/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartParams.ts deleted file mode 100644 index 3153a20..0000000 --- a/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; - -export type WindowsSandboxSetupStartParams = { - mode: WindowsSandboxSetupMode; - cwd?: AbsolutePathBuf | null; -}; diff --git a/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartResponse.ts b/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartResponse.ts deleted file mode 100644 index 1aec14c..0000000 --- a/src/runtimes/openai/generated/v2/WindowsSandboxSetupStartResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WindowsSandboxSetupStartResponse = { started: boolean }; diff --git a/src/runtimes/openai/generated/v2/WindowsWorldWritableWarningNotification.ts b/src/runtimes/openai/generated/v2/WindowsWorldWritableWarningNotification.ts deleted file mode 100644 index b61824c..0000000 --- a/src/runtimes/openai/generated/v2/WindowsWorldWritableWarningNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WindowsWorldWritableWarningNotification = { - samplePaths: Array; - extraCount: number; - failedScan: boolean; -}; diff --git a/src/runtimes/openai/generated/v2/WorkspaceMessage.ts b/src/runtimes/openai/generated/v2/WorkspaceMessage.ts deleted file mode 100644 index f8d54d1..0000000 --- a/src/runtimes/openai/generated/v2/WorkspaceMessage.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WorkspaceMessageType } from "./WorkspaceMessageType"; - -export type WorkspaceMessage = { - messageId: string; - messageType: WorkspaceMessageType; - messageBody: string; - /** - * Unix timestamp (in seconds) when the message was created. - */ - createdAt: number | null; - /** - * Unix timestamp (in seconds) when the message was archived. - */ - archivedAt: number | null; -}; diff --git a/src/runtimes/openai/generated/v2/WorkspaceMessageType.ts b/src/runtimes/openai/generated/v2/WorkspaceMessageType.ts deleted file mode 100644 index 9d9438d..0000000 --- a/src/runtimes/openai/generated/v2/WorkspaceMessageType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WorkspaceMessageType = "headline" | "announcement" | "unknown"; diff --git a/src/runtimes/openai/generated/v2/WriteStatus.ts b/src/runtimes/openai/generated/v2/WriteStatus.ts deleted file mode 100644 index 068eb3b..0000000 --- a/src/runtimes/openai/generated/v2/WriteStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WriteStatus = "ok" | "okOverridden"; diff --git a/src/runtimes/openai/generated/v2/index.ts b/src/runtimes/openai/generated/v2/index.ts deleted file mode 100644 index abe112f..0000000 --- a/src/runtimes/openai/generated/v2/index.ts +++ /dev/null @@ -1,714 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -export type { Account } from "./Account"; -export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification"; -export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification"; -export type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket"; -export type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary"; -export type { AccountUpdatedNotification } from "./AccountUpdatedNotification"; -export type { ActivePermissionProfile } from "./ActivePermissionProfile"; -export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType"; -export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus"; -export type { AdditionalContextEntry } from "./AdditionalContextEntry"; -export type { AdditionalContextKind } from "./AdditionalContextKind"; -export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; -export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; -export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; -export type { AgentMessageDelivery } from "./AgentMessageDelivery"; -export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification"; -export type { AllowDenyRequirement } from "./AllowDenyRequirement"; -export type { AnalyticsConfig } from "./AnalyticsConfig"; -export type { AppBranding } from "./AppBranding"; -export type { AppInfo } from "./AppInfo"; -export type { AppListUpdatedNotification } from "./AppListUpdatedNotification"; -export type { AppMetadata } from "./AppMetadata"; -export type { AppReview } from "./AppReview"; -export type { AppScreenshot } from "./AppScreenshot"; -export type { AppSummary } from "./AppSummary"; -export type { AppTemplateSummary } from "./AppTemplateSummary"; -export type { AppTemplateUnavailableReason } from "./AppTemplateUnavailableReason"; -export type { AppToolApproval } from "./AppToolApproval"; -export type { AppToolSummary } from "./AppToolSummary"; -export type { AppToolsConfig } from "./AppToolsConfig"; -export type { ApprovalsReviewer } from "./ApprovalsReviewer"; -export type { AppsConfig } from "./AppsConfig"; -export type { AppsDefaultConfig } from "./AppsDefaultConfig"; -export type { AppsInstalledParams } from "./AppsInstalledParams"; -export type { AppsInstalledResponse } from "./AppsInstalledResponse"; -export type { AppsListParams } from "./AppsListParams"; -export type { AppsListResponse } from "./AppsListResponse"; -export type { AppsReadParams } from "./AppsReadParams"; -export type { AppsReadResponse } from "./AppsReadResponse"; -export type { AskForApproval } from "./AskForApproval"; -export type { AttestationGenerateParams } from "./AttestationGenerateParams"; -export type { AttestationGenerateResponse } from "./AttestationGenerateResponse"; -export type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource"; -export type { AutoReviewRequirements } from "./AutoReviewRequirements"; -export type { AwsCredentialType } from "./AwsCredentialType"; -export type { BedrockAwsProfile } from "./BedrockAwsProfile"; -export type { BedrockDiscoverParams } from "./BedrockDiscoverParams"; -export type { BedrockDiscoverResponse } from "./BedrockDiscoverResponse"; -export type { BedrockEnvironmentCredential } from "./BedrockEnvironmentCredential"; -export type { BedrockSetupParams } from "./BedrockSetupParams"; -export type { BedrockSetupResponse } from "./BedrockSetupResponse"; -export type { BrowserUseAccessApprovalLifetime } from "./BrowserUseAccessApprovalLifetime"; -export type { BrowserUseConfig } from "./BrowserUseConfig"; -export type { BrowserUseOriginPolicy } from "./BrowserUseOriginPolicy"; -export type { BrowserUseOriginPolicyConfig } from "./BrowserUseOriginPolicyConfig"; -export type { BrowserUseRequirements } from "./BrowserUseRequirements"; -export type { ByteRange } from "./ByteRange"; -export type { CancelLoginAccountParams } from "./CancelLoginAccountParams"; -export type { CancelLoginAccountResponse } from "./CancelLoginAccountResponse"; -export type { CancelLoginAccountStatus } from "./CancelLoginAccountStatus"; -export type { CapabilityRootLocation } from "./CapabilityRootLocation"; -export type { ChatgptAuthTokensRefreshParams } from "./ChatgptAuthTokensRefreshParams"; -export type { ChatgptAuthTokensRefreshReason } from "./ChatgptAuthTokensRefreshReason"; -export type { ChatgptAuthTokensRefreshResponse } from "./ChatgptAuthTokensRefreshResponse"; -export type { CliAuthCredentialsStoreMode } from "./CliAuthCredentialsStoreMode"; -export type { CodexErrorInfo } from "./CodexErrorInfo"; -export type { CollabAgentState } from "./CollabAgentState"; -export type { CollabAgentStatus } from "./CollabAgentStatus"; -export type { CollabAgentTool } from "./CollabAgentTool"; -export type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; -export type { CollaborationModeListParams } from "./CollaborationModeListParams"; -export type { CollaborationModeListResponse } from "./CollaborationModeListResponse"; -export type { CollaborationModeMask } from "./CollaborationModeMask"; -export type { CommandAction } from "./CommandAction"; -export type { CommandExecOutputDeltaNotification } from "./CommandExecOutputDeltaNotification"; -export type { CommandExecOutputStream } from "./CommandExecOutputStream"; -export type { CommandExecParams } from "./CommandExecParams"; -export type { CommandExecResizeParams } from "./CommandExecResizeParams"; -export type { CommandExecResizeResponse } from "./CommandExecResizeResponse"; -export type { CommandExecResponse } from "./CommandExecResponse"; -export type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; -export type { CommandExecTerminateParams } from "./CommandExecTerminateParams"; -export type { CommandExecTerminateResponse } from "./CommandExecTerminateResponse"; -export type { CommandExecWriteParams } from "./CommandExecWriteParams"; -export type { CommandExecWriteResponse } from "./CommandExecWriteResponse"; -export type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; -export type { CommandExecutionApprovalKind } from "./CommandExecutionApprovalKind"; -export type { CommandExecutionOutputDeltaNotification } from "./CommandExecutionOutputDeltaNotification"; -export type { CommandExecutionRequestApprovalParams } from "./CommandExecutionRequestApprovalParams"; -export type { CommandExecutionRequestApprovalResponse } from "./CommandExecutionRequestApprovalResponse"; -export type { CommandExecutionSource } from "./CommandExecutionSource"; -export type { CommandExecutionStatus } from "./CommandExecutionStatus"; -export type { CommandMigration } from "./CommandMigration"; -export type { ComputerUseConfig } from "./ComputerUseConfig"; -export type { ComputerUseMacosConfig } from "./ComputerUseMacosConfig"; -export type { ComputerUseMacosRequirements } from "./ComputerUseMacosRequirements"; -export type { ComputerUseRequirements } from "./ComputerUseRequirements"; -export type { ComputerUseWindowsConfig } from "./ComputerUseWindowsConfig"; -export type { ComputerUseWindowsExeConfig } from "./ComputerUseWindowsExeConfig"; -export type { ComputerUseWindowsExeRequirement } from "./ComputerUseWindowsExeRequirement"; -export type { ComputerUseWindowsRequirements } from "./ComputerUseWindowsRequirements"; -export type { Config } from "./Config"; -export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams"; -export type { ConfigEdit } from "./ConfigEdit"; -export type { ConfigLayer } from "./ConfigLayer"; -export type { ConfigLayerMetadata } from "./ConfigLayerMetadata"; -export type { ConfigLayerSource } from "./ConfigLayerSource"; -export type { ConfigReadParams } from "./ConfigReadParams"; -export type { ConfigReadResponse } from "./ConfigReadResponse"; -export type { ConfigRequirements } from "./ConfigRequirements"; -export type { ConfigRequirementsReadResponse } from "./ConfigRequirementsReadResponse"; -export type { ConfigValueWriteParams } from "./ConfigValueWriteParams"; -export type { ConfigWarningNotification } from "./ConfigWarningNotification"; -export type { ConfigWriteResponse } from "./ConfigWriteResponse"; -export type { ConfiguredHookHandler } from "./ConfiguredHookHandler"; -export type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup"; -export type { ConnectorMetadata } from "./ConnectorMetadata"; -export type { ConsumeAccountRateLimitResetCreditOutcome } from "./ConsumeAccountRateLimitResetCreditOutcome"; -export type { ConsumeAccountRateLimitResetCreditParams } from "./ConsumeAccountRateLimitResetCreditParams"; -export type { ConsumeAccountRateLimitResetCreditResponse } from "./ConsumeAccountRateLimitResetCreditResponse"; -export type { ContextCompactedNotification } from "./ContextCompactedNotification"; -export type { CreditsSnapshot } from "./CreditsSnapshot"; -export type { CurrentTimeReadParams } from "./CurrentTimeReadParams"; -export type { CurrentTimeReadResponse } from "./CurrentTimeReadResponse"; -export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification"; -export type { DesktopOnboardingEntrypoint } from "./DesktopOnboardingEntrypoint"; -export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; -export type { DynamicToolCallParams } from "./DynamicToolCallParams"; -export type { DynamicToolCallResponse } from "./DynamicToolCallResponse"; -export type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; -export type { DynamicToolFunctionSpec } from "./DynamicToolFunctionSpec"; -export type { DynamicToolNamespaceSpec } from "./DynamicToolNamespaceSpec"; -export type { DynamicToolNamespaceTool } from "./DynamicToolNamespaceTool"; -export type { DynamicToolSpec } from "./DynamicToolSpec"; -export type { EnvironmentAddParams } from "./EnvironmentAddParams"; -export type { EnvironmentAddResponse } from "./EnvironmentAddResponse"; -export type { EnvironmentConnectionNotification } from "./EnvironmentConnectionNotification"; -export type { EnvironmentInfoParams } from "./EnvironmentInfoParams"; -export type { EnvironmentInfoResponse } from "./EnvironmentInfoResponse"; -export type { EnvironmentShellInfo } from "./EnvironmentShellInfo"; -export type { EnvironmentStatusKind } from "./EnvironmentStatusKind"; -export type { EnvironmentStatusParams } from "./EnvironmentStatusParams"; -export type { EnvironmentStatusResponse } from "./EnvironmentStatusResponse"; -export type { ErrorNotification } from "./ErrorNotification"; -export type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; -export type { ExperimentalFeature } from "./ExperimentalFeature"; -export type { ExperimentalFeatureEnablementSetParams } from "./ExperimentalFeatureEnablementSetParams"; -export type { ExperimentalFeatureEnablementSetResponse } from "./ExperimentalFeatureEnablementSetResponse"; -export type { ExperimentalFeatureListParams } from "./ExperimentalFeatureListParams"; -export type { ExperimentalFeatureListResponse } from "./ExperimentalFeatureListResponse"; -export type { ExperimentalFeatureStage } from "./ExperimentalFeatureStage"; -export type { ExternalAgentConfigDetectParams } from "./ExternalAgentConfigDetectParams"; -export type { ExternalAgentConfigDetectResponse } from "./ExternalAgentConfigDetectResponse"; -export type { ExternalAgentConfigImportCompletedNotification } from "./ExternalAgentConfigImportCompletedNotification"; -export type { ExternalAgentConfigImportHistoriesReadResponse } from "./ExternalAgentConfigImportHistoriesReadResponse"; -export type { ExternalAgentConfigImportHistory } from "./ExternalAgentConfigImportHistory"; -export type { ExternalAgentConfigImportHistoryRecordParams } from "./ExternalAgentConfigImportHistoryRecordParams"; -export type { ExternalAgentConfigImportHistoryRecordResponse } from "./ExternalAgentConfigImportHistoryRecordResponse"; -export type { ExternalAgentConfigImportHistoryRecordSuccessParams } from "./ExternalAgentConfigImportHistoryRecordSuccessParams"; -export type { ExternalAgentConfigImportHistoryRecordTypeResultParams } from "./ExternalAgentConfigImportHistoryRecordTypeResultParams"; -export type { ExternalAgentConfigImportItemTypeFailure } from "./ExternalAgentConfigImportItemTypeFailure"; -export type { ExternalAgentConfigImportItemTypeSuccess } from "./ExternalAgentConfigImportItemTypeSuccess"; -export type { ExternalAgentConfigImportParams } from "./ExternalAgentConfigImportParams"; -export type { ExternalAgentConfigImportProgressNotification } from "./ExternalAgentConfigImportProgressNotification"; -export type { ExternalAgentConfigImportResponse } from "./ExternalAgentConfigImportResponse"; -export type { ExternalAgentConfigImportTypeResult } from "./ExternalAgentConfigImportTypeResult"; -export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; -export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; -export type { ExternalAgentDetectedConnectorCandidate } from "./ExternalAgentDetectedConnectorCandidate"; -export type { ExternalAgentDetectedConnectorSource } from "./ExternalAgentDetectedConnectorSource"; -export type { ExternalAgentImportedConnectorCandidate } from "./ExternalAgentImportedConnectorCandidate"; -export type { ExternalAgentImportedConnectorSource } from "./ExternalAgentImportedConnectorSource"; -export type { FeedbackRequirements } from "./FeedbackRequirements"; -export type { FeedbackUploadParams } from "./FeedbackUploadParams"; -export type { FeedbackUploadResponse } from "./FeedbackUploadResponse"; -export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision"; -export type { FileChangeOutputDeltaNotification } from "./FileChangeOutputDeltaNotification"; -export type { FileChangePatchUpdatedNotification } from "./FileChangePatchUpdatedNotification"; -export type { FileChangeRequestApprovalParams } from "./FileChangeRequestApprovalParams"; -export type { FileChangeRequestApprovalResponse } from "./FileChangeRequestApprovalResponse"; -export type { FileSystemAccessMode } from "./FileSystemAccessMode"; -export type { FileSystemPath } from "./FileSystemPath"; -export type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry"; -export type { FileSystemSpecialPath } from "./FileSystemSpecialPath"; -export type { FileUpdateChange } from "./FileUpdateChange"; -export type { ForcedChatgptWorkspaceIds } from "./ForcedChatgptWorkspaceIds"; -export type { FsChangedNotification } from "./FsChangedNotification"; -export type { FsCopyParams } from "./FsCopyParams"; -export type { FsCopyResponse } from "./FsCopyResponse"; -export type { FsCreateDirectoryParams } from "./FsCreateDirectoryParams"; -export type { FsCreateDirectoryResponse } from "./FsCreateDirectoryResponse"; -export type { FsGetMetadataParams } from "./FsGetMetadataParams"; -export type { FsGetMetadataResponse } from "./FsGetMetadataResponse"; -export type { FsReadDirectoryEntry } from "./FsReadDirectoryEntry"; -export type { FsReadDirectoryParams } from "./FsReadDirectoryParams"; -export type { FsReadDirectoryResponse } from "./FsReadDirectoryResponse"; -export type { FsReadFileParams } from "./FsReadFileParams"; -export type { FsReadFileResponse } from "./FsReadFileResponse"; -export type { FsRemoveParams } from "./FsRemoveParams"; -export type { FsRemoveResponse } from "./FsRemoveResponse"; -export type { FsUnwatchParams } from "./FsUnwatchParams"; -export type { FsUnwatchResponse } from "./FsUnwatchResponse"; -export type { FsWatchParams } from "./FsWatchParams"; -export type { FsWatchResponse } from "./FsWatchResponse"; -export type { FsWriteFileParams } from "./FsWriteFileParams"; -export type { FsWriteFileResponse } from "./FsWriteFileResponse"; -export type { GetAccountParams } from "./GetAccountParams"; -export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse"; -export type { GetAccountResponse } from "./GetAccountResponse"; -export type { GetAccountTokenUsageParams } from "./GetAccountTokenUsageParams"; -export type { GetAccountTokenUsageResponse } from "./GetAccountTokenUsageResponse"; -export type { GetWorkspaceMessagesResponse } from "./GetWorkspaceMessagesResponse"; -export type { GitInfo } from "./GitInfo"; -export type { GrantedPermissionProfile } from "./GrantedPermissionProfile"; -export type { GuardianApprovalReview } from "./GuardianApprovalReview"; -export type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction"; -export type { GuardianApprovalReviewStatus } from "./GuardianApprovalReviewStatus"; -export type { GuardianCommandSource } from "./GuardianCommandSource"; -export type { GuardianRiskLevel } from "./GuardianRiskLevel"; -export type { GuardianUserAuthorization } from "./GuardianUserAuthorization"; -export type { GuardianWarningNotification } from "./GuardianWarningNotification"; -export type { HookCompletedNotification } from "./HookCompletedNotification"; -export type { HookErrorInfo } from "./HookErrorInfo"; -export type { HookEventName } from "./HookEventName"; -export type { HookExecutionMode } from "./HookExecutionMode"; -export type { HookHandlerType } from "./HookHandlerType"; -export type { HookMetadata } from "./HookMetadata"; -export type { HookMigration } from "./HookMigration"; -export type { HookOutputEntry } from "./HookOutputEntry"; -export type { HookOutputEntryKind } from "./HookOutputEntryKind"; -export type { HookPromptFragment } from "./HookPromptFragment"; -export type { HookRunStatus } from "./HookRunStatus"; -export type { HookRunSummary } from "./HookRunSummary"; -export type { HookScope } from "./HookScope"; -export type { HookSource } from "./HookSource"; -export type { HookStartedNotification } from "./HookStartedNotification"; -export type { HookTrustStatus } from "./HookTrustStatus"; -export type { HooksListEntry } from "./HooksListEntry"; -export type { HooksListParams } from "./HooksListParams"; -export type { HooksListResponse } from "./HooksListResponse"; -export type { InAppBrowserRequirements } from "./InAppBrowserRequirements"; -export type { InstalledApp } from "./InstalledApp"; -export type { ItemCompletedNotification } from "./ItemCompletedNotification"; -export type { ItemGuardianApprovalReviewCompletedNotification } from "./ItemGuardianApprovalReviewCompletedNotification"; -export type { ItemGuardianApprovalReviewStartedNotification } from "./ItemGuardianApprovalReviewStartedNotification"; -export type { ItemStartedNotification } from "./ItemStartedNotification"; -export type { ListMcpServerStatusParams } from "./ListMcpServerStatusParams"; -export type { ListMcpServerStatusResponse } from "./ListMcpServerStatusResponse"; -export type { LoginAccountParams } from "./LoginAccountParams"; -export type { LoginAccountResponse } from "./LoginAccountResponse"; -export type { LoginAppBrand } from "./LoginAppBrand"; -export type { LogoutAccountResponse } from "./LogoutAccountResponse"; -export type { ManagedHooksRequirements } from "./ManagedHooksRequirements"; -export type { MarketplaceAddParams } from "./MarketplaceAddParams"; -export type { MarketplaceAddResponse } from "./MarketplaceAddResponse"; -export type { MarketplaceInterface } from "./MarketplaceInterface"; -export type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo"; -export type { MarketplaceRemoveParams } from "./MarketplaceRemoveParams"; -export type { MarketplaceRemoveResponse } from "./MarketplaceRemoveResponse"; -export type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo"; -export type { MarketplaceUpgradeParams } from "./MarketplaceUpgradeParams"; -export type { MarketplaceUpgradeResponse } from "./MarketplaceUpgradeResponse"; -export type { McpAuthStatus } from "./McpAuthStatus"; -export type { McpElicitationArrayType } from "./McpElicitationArrayType"; -export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema"; -export type { McpElicitationBooleanType } from "./McpElicitationBooleanType"; -export type { McpElicitationConstOption } from "./McpElicitationConstOption"; -export type { McpElicitationEnumSchema } from "./McpElicitationEnumSchema"; -export type { McpElicitationLegacyTitledEnumSchema } from "./McpElicitationLegacyTitledEnumSchema"; -export type { McpElicitationMultiSelectEnumSchema } from "./McpElicitationMultiSelectEnumSchema"; -export type { McpElicitationNumberSchema } from "./McpElicitationNumberSchema"; -export type { McpElicitationNumberType } from "./McpElicitationNumberType"; -export type { McpElicitationObjectType } from "./McpElicitationObjectType"; -export type { McpElicitationPrimitiveSchema } from "./McpElicitationPrimitiveSchema"; -export type { McpElicitationSchema } from "./McpElicitationSchema"; -export type { McpElicitationSingleSelectEnumSchema } from "./McpElicitationSingleSelectEnumSchema"; -export type { McpElicitationStringFormat } from "./McpElicitationStringFormat"; -export type { McpElicitationStringSchema } from "./McpElicitationStringSchema"; -export type { McpElicitationStringType } from "./McpElicitationStringType"; -export type { McpElicitationTitledEnumItems } from "./McpElicitationTitledEnumItems"; -export type { McpElicitationTitledMultiSelectEnumSchema } from "./McpElicitationTitledMultiSelectEnumSchema"; -export type { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitationTitledSingleSelectEnumSchema"; -export type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems"; -export type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema"; -export type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema"; -export type { McpResourceReadParams } from "./McpResourceReadParams"; -export type { McpResourceReadResponse } from "./McpResourceReadResponse"; -export type { McpServerConnectionStatus } from "./McpServerConnectionStatus"; -export type { McpServerElicitationAction } from "./McpServerElicitationAction"; -export type { McpServerElicitationRequestParams } from "./McpServerElicitationRequestParams"; -export type { McpServerElicitationRequestResponse } from "./McpServerElicitationRequestResponse"; -export type { McpServerEventNotification } from "./McpServerEventNotification"; -export type { McpServerEventStreamNotification } from "./McpServerEventStreamNotification"; -export type { McpServerEventStreamStartParams } from "./McpServerEventStreamStartParams"; -export type { McpServerEventStreamStartResponse } from "./McpServerEventStreamStartResponse"; -export type { McpServerEventStreamStopParams } from "./McpServerEventStreamStopParams"; -export type { McpServerEventStreamStopResponse } from "./McpServerEventStreamStopResponse"; -export type { McpServerMigration } from "./McpServerMigration"; -export type { McpServerOauthClientRegistration } from "./McpServerOauthClientRegistration"; -export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthLoginCompletedNotification"; -export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams"; -export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse"; -export type { McpServerRefreshResponse } from "./McpServerRefreshResponse"; -export type { McpServerStartupFailureReason } from "./McpServerStartupFailureReason"; -export type { McpServerStartupState } from "./McpServerStartupState"; -export type { McpServerStatus } from "./McpServerStatus"; -export type { McpServerStatusDetail } from "./McpServerStatusDetail"; -export type { McpServerStatusUpdatedNotification } from "./McpServerStatusUpdatedNotification"; -export type { McpServerToolCallParams } from "./McpServerToolCallParams"; -export type { McpServerToolCallResponse } from "./McpServerToolCallResponse"; -export type { McpToolCallAppContext } from "./McpToolCallAppContext"; -export type { McpToolCallError } from "./McpToolCallError"; -export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification"; -export type { McpToolCallResult } from "./McpToolCallResult"; -export type { McpToolCallStatus } from "./McpToolCallStatus"; -export type { MemoryCitation } from "./MemoryCitation"; -export type { MemoryCitationEntry } from "./MemoryCitationEntry"; -export type { MemoryResetResponse } from "./MemoryResetResponse"; -export type { MergeStrategy } from "./MergeStrategy"; -export type { MigrationDetails } from "./MigrationDetails"; -export type { MockExperimentalMethodParams } from "./MockExperimentalMethodParams"; -export type { MockExperimentalMethodResponse } from "./MockExperimentalMethodResponse"; -export type { Model } from "./Model"; -export type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; -export type { ModelListParams } from "./ModelListParams"; -export type { ModelListResponse } from "./ModelListResponse"; -export type { ModelProviderCapabilitiesReadParams } from "./ModelProviderCapabilitiesReadParams"; -export type { ModelProviderCapabilitiesReadResponse } from "./ModelProviderCapabilitiesReadResponse"; -export type { ModelRerouteReason } from "./ModelRerouteReason"; -export type { ModelReroutedNotification } from "./ModelReroutedNotification"; -export type { ModelSafetyBufferingUpdatedNotification } from "./ModelSafetyBufferingUpdatedNotification"; -export type { ModelServiceTier } from "./ModelServiceTier"; -export type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; -export type { ModelVerification } from "./ModelVerification"; -export type { ModelVerificationNotification } from "./ModelVerificationNotification"; -export type { ModelsRequirements } from "./ModelsRequirements"; -export type { MultiAgentVersion } from "./MultiAgentVersion"; -export type { NetworkAccess } from "./NetworkAccess"; -export type { NetworkApprovalContext } from "./NetworkApprovalContext"; -export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; -export type { NetworkDomainPermission } from "./NetworkDomainPermission"; -export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; -export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; -export type { NetworkRequirements } from "./NetworkRequirements"; -export type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission"; -export type { NewThreadModelDefaults } from "./NewThreadModelDefaults"; -export type { NonSteerableTurnKind } from "./NonSteerableTurnKind"; -export type { OverriddenMetadata } from "./OverriddenMetadata"; -export type { PatchApplyStatus } from "./PatchApplyStatus"; -export type { PatchChangeKind } from "./PatchChangeKind"; -export type { PermissionGrantScope } from "./PermissionGrantScope"; -export type { PermissionProfileListParams } from "./PermissionProfileListParams"; -export type { PermissionProfileListResponse } from "./PermissionProfileListResponse"; -export type { PermissionProfileSummary } from "./PermissionProfileSummary"; -export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams"; -export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse"; -export type { PlanDeltaNotification } from "./PlanDeltaNotification"; -export type { PluginAuthPolicy } from "./PluginAuthPolicy"; -export type { PluginAvailability } from "./PluginAvailability"; -export type { PluginDetail } from "./PluginDetail"; -export type { PluginDisabledReason } from "./PluginDisabledReason"; -export type { PluginHookSummary } from "./PluginHookSummary"; -export type { PluginInstallParams } from "./PluginInstallParams"; -export type { PluginInstallPolicy } from "./PluginInstallPolicy"; -export type { PluginInstallPolicySource } from "./PluginInstallPolicySource"; -export type { PluginInstallResponse } from "./PluginInstallResponse"; -export type { PluginInstalledParams } from "./PluginInstalledParams"; -export type { PluginInstalledResponse } from "./PluginInstalledResponse"; -export type { PluginInterface } from "./PluginInterface"; -export type { PluginListMarketplaceKind } from "./PluginListMarketplaceKind"; -export type { PluginListParams } from "./PluginListParams"; -export type { PluginListResponse } from "./PluginListResponse"; -export type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry"; -export type { PluginReadParams } from "./PluginReadParams"; -export type { PluginReadResponse } from "./PluginReadResponse"; -export type { PluginSearchParams } from "./PluginSearchParams"; -export type { PluginSearchResponse } from "./PluginSearchResponse"; -export type { PluginSearchResult } from "./PluginSearchResult"; -export type { PluginSearchScope } from "./PluginSearchScope"; -export type { PluginShareCheckoutParams } from "./PluginShareCheckoutParams"; -export type { PluginShareCheckoutResponse } from "./PluginShareCheckoutResponse"; -export type { PluginShareContext } from "./PluginShareContext"; -export type { PluginShareDeleteParams } from "./PluginShareDeleteParams"; -export type { PluginShareDeleteResponse } from "./PluginShareDeleteResponse"; -export type { PluginShareDiscoverability } from "./PluginShareDiscoverability"; -export type { PluginShareListItem } from "./PluginShareListItem"; -export type { PluginShareListParams } from "./PluginShareListParams"; -export type { PluginShareListResponse } from "./PluginShareListResponse"; -export type { PluginSharePrincipal } from "./PluginSharePrincipal"; -export type { PluginSharePrincipalRole } from "./PluginSharePrincipalRole"; -export type { PluginSharePrincipalType } from "./PluginSharePrincipalType"; -export type { PluginShareSaveParams } from "./PluginShareSaveParams"; -export type { PluginShareSaveResponse } from "./PluginShareSaveResponse"; -export type { PluginShareTarget } from "./PluginShareTarget"; -export type { PluginShareTargetRole } from "./PluginShareTargetRole"; -export type { PluginShareUpdateDiscoverability } from "./PluginShareUpdateDiscoverability"; -export type { PluginShareUpdateTargetsParams } from "./PluginShareUpdateTargetsParams"; -export type { PluginShareUpdateTargetsResponse } from "./PluginShareUpdateTargetsResponse"; -export type { PluginSkillReadParams } from "./PluginSkillReadParams"; -export type { PluginSkillReadResponse } from "./PluginSkillReadResponse"; -export type { PluginSource } from "./PluginSource"; -export type { PluginSummary } from "./PluginSummary"; -export type { PluginUninstallParams } from "./PluginUninstallParams"; -export type { PluginUninstallResponse } from "./PluginUninstallResponse"; -export type { PluginsMigration } from "./PluginsMigration"; -export type { ProcessExitedNotification } from "./ProcessExitedNotification"; -export type { ProcessKillParams } from "./ProcessKillParams"; -export type { ProcessKillResponse } from "./ProcessKillResponse"; -export type { ProcessOutputDeltaNotification } from "./ProcessOutputDeltaNotification"; -export type { ProcessOutputStream } from "./ProcessOutputStream"; -export type { ProcessResizePtyParams } from "./ProcessResizePtyParams"; -export type { ProcessResizePtyResponse } from "./ProcessResizePtyResponse"; -export type { ProcessSpawnParams } from "./ProcessSpawnParams"; -export type { ProcessSpawnResponse } from "./ProcessSpawnResponse"; -export type { ProcessTerminalSize } from "./ProcessTerminalSize"; -export type { ProcessWriteStdinParams } from "./ProcessWriteStdinParams"; -export type { ProcessWriteStdinResponse } from "./ProcessWriteStdinResponse"; -export type { Project } from "./Project"; -export type { ProjectChangeType } from "./ProjectChangeType"; -export type { ProjectChangedNotification } from "./ProjectChangedNotification"; -export type { ProjectCreateParams } from "./ProjectCreateParams"; -export type { ProjectCreateResponse } from "./ProjectCreateResponse"; -export type { ProjectDeleteParams } from "./ProjectDeleteParams"; -export type { ProjectDeleteResponse } from "./ProjectDeleteResponse"; -export type { ProjectImportParams } from "./ProjectImportParams"; -export type { ProjectImportResponse } from "./ProjectImportResponse"; -export type { ProjectListParams } from "./ProjectListParams"; -export type { ProjectListResponse } from "./ProjectListResponse"; -export type { ProjectMoveParams } from "./ProjectMoveParams"; -export type { ProjectMoveResponse } from "./ProjectMoveResponse"; -export type { ProjectReadParams } from "./ProjectReadParams"; -export type { ProjectReadResponse } from "./ProjectReadResponse"; -export type { ProjectRoot } from "./ProjectRoot"; -export type { ProjectUpdateParams } from "./ProjectUpdateParams"; -export type { ProjectUpdateResponse } from "./ProjectUpdateResponse"; -export type { QueuedSubmission } from "./QueuedSubmission"; -export type { RateLimitReachedType } from "./RateLimitReachedType"; -export type { RateLimitResetCredit } from "./RateLimitResetCredit"; -export type { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus"; -export type { RateLimitResetCreditsSummary } from "./RateLimitResetCreditsSummary"; -export type { RateLimitResetType } from "./RateLimitResetType"; -export type { RateLimitSnapshot } from "./RateLimitSnapshot"; -export type { RateLimitWindow } from "./RateLimitWindow"; -export type { RawResponseCompletedNotification } from "./RawResponseCompletedNotification"; -export type { RawResponseItemCompletedNotification } from "./RawResponseItemCompletedNotification"; -export type { ReasoningEffortOption } from "./ReasoningEffortOption"; -export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification"; -export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification"; -export type { ReasoningTextDeltaNotification } from "./ReasoningTextDeltaNotification"; -export type { RemoteControlClient } from "./RemoteControlClient"; -export type { RemoteControlClientsListOrder } from "./RemoteControlClientsListOrder"; -export type { RemoteControlClientsListParams } from "./RemoteControlClientsListParams"; -export type { RemoteControlClientsListResponse } from "./RemoteControlClientsListResponse"; -export type { RemoteControlClientsRevokeParams } from "./RemoteControlClientsRevokeParams"; -export type { RemoteControlClientsRevokeResponse } from "./RemoteControlClientsRevokeResponse"; -export type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; -export type { RemoteControlDisableParams } from "./RemoteControlDisableParams"; -export type { RemoteControlDisableResponse } from "./RemoteControlDisableResponse"; -export type { RemoteControlEnableParams } from "./RemoteControlEnableParams"; -export type { RemoteControlEnableResponse } from "./RemoteControlEnableResponse"; -export type { RemoteControlPairingStartParams } from "./RemoteControlPairingStartParams"; -export type { RemoteControlPairingStartResponse } from "./RemoteControlPairingStartResponse"; -export type { RemoteControlPairingStatusParams } from "./RemoteControlPairingStatusParams"; -export type { RemoteControlPairingStatusResponse } from "./RemoteControlPairingStatusResponse"; -export type { RemoteControlStatusChangedNotification } from "./RemoteControlStatusChangedNotification"; -export type { RemoteControlStatusReadResponse } from "./RemoteControlStatusReadResponse"; -export type { RequestPermissionProfile } from "./RequestPermissionProfile"; -export type { ResidencyRequirement } from "./ResidencyRequirement"; -export type { ReviewDelivery } from "./ReviewDelivery"; -export type { ReviewStartParams } from "./ReviewStartParams"; -export type { ReviewStartResponse } from "./ReviewStartResponse"; -export type { ReviewTarget } from "./ReviewTarget"; -export type { SandboxMode } from "./SandboxMode"; -export type { SandboxPolicy } from "./SandboxPolicy"; -export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; -export type { ScheduledTaskSchedule } from "./ScheduledTaskSchedule"; -export type { ScheduledTaskSummary } from "./ScheduledTaskSummary"; -export type { ScheduledTaskWeekday } from "./ScheduledTaskWeekday"; -export type { SelectedCapabilityRoot } from "./SelectedCapabilityRoot"; -export type { SendAddCreditsNudgeEmailParams } from "./SendAddCreditsNudgeEmailParams"; -export type { SendAddCreditsNudgeEmailResponse } from "./SendAddCreditsNudgeEmailResponse"; -export type { ServerDiagnosticsGauge } from "./ServerDiagnosticsGauge"; -export type { ServerDiagnosticsParams } from "./ServerDiagnosticsParams"; -export type { ServerDiagnosticsProcess } from "./ServerDiagnosticsProcess"; -export type { ServerDiagnosticsResponse } from "./ServerDiagnosticsResponse"; -export type { ServerRequestResolvedNotification } from "./ServerRequestResolvedNotification"; -export type { SessionMigration } from "./SessionMigration"; -export type { SessionSource } from "./SessionSource"; -export type { SkillDependencies } from "./SkillDependencies"; -export type { SkillErrorInfo } from "./SkillErrorInfo"; -export type { SkillInterface } from "./SkillInterface"; -export type { SkillMetadata } from "./SkillMetadata"; -export type { SkillMigration } from "./SkillMigration"; -export type { SkillScope } from "./SkillScope"; -export type { SkillSummary } from "./SkillSummary"; -export type { SkillToolDependency } from "./SkillToolDependency"; -export type { SkillsChangedNotification } from "./SkillsChangedNotification"; -export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams"; -export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse"; -export type { SkillsExtraRootsSetParams } from "./SkillsExtraRootsSetParams"; -export type { SkillsExtraRootsSetResponse } from "./SkillsExtraRootsSetResponse"; -export type { SkillsListEntry } from "./SkillsListEntry"; -export type { SkillsListParams } from "./SkillsListParams"; -export type { SkillsListResponse } from "./SkillsListResponse"; -export type { SortDirection } from "./SortDirection"; -export type { SpendControlLimitSnapshot } from "./SpendControlLimitSnapshot"; -export type { StrictReviewRequiredNotification } from "./StrictReviewRequiredNotification"; -export type { SubAgentActivityKind } from "./SubAgentActivityKind"; -export type { SubagentMigration } from "./SubagentMigration"; -export type { TerminalInteractionNotification } from "./TerminalInteractionNotification"; -export type { TextElement } from "./TextElement"; -export type { TextPosition } from "./TextPosition"; -export type { TextRange } from "./TextRange"; -export type { Thread } from "./Thread"; -export type { ThreadActiveFlag } from "./ThreadActiveFlag"; -export type { ThreadApproveGuardianDeniedActionParams } from "./ThreadApproveGuardianDeniedActionParams"; -export type { ThreadApproveGuardianDeniedActionResponse } from "./ThreadApproveGuardianDeniedActionResponse"; -export type { ThreadArchiveParams } from "./ThreadArchiveParams"; -export type { ThreadArchiveResponse } from "./ThreadArchiveResponse"; -export type { ThreadArchivedNotification } from "./ThreadArchivedNotification"; -export type { ThreadBackgroundTerminal } from "./ThreadBackgroundTerminal"; -export type { ThreadBackgroundTerminalsCleanParams } from "./ThreadBackgroundTerminalsCleanParams"; -export type { ThreadBackgroundTerminalsCleanResponse } from "./ThreadBackgroundTerminalsCleanResponse"; -export type { ThreadBackgroundTerminalsListParams } from "./ThreadBackgroundTerminalsListParams"; -export type { ThreadBackgroundTerminalsListResponse } from "./ThreadBackgroundTerminalsListResponse"; -export type { ThreadBackgroundTerminalsTerminateParams } from "./ThreadBackgroundTerminalsTerminateParams"; -export type { ThreadBackgroundTerminalsTerminateResponse } from "./ThreadBackgroundTerminalsTerminateResponse"; -export type { ThreadClosedNotification } from "./ThreadClosedNotification"; -export type { ThreadCompactStartParams } from "./ThreadCompactStartParams"; -export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse"; -export type { ThreadDecrementElicitationParams } from "./ThreadDecrementElicitationParams"; -export type { ThreadDecrementElicitationResponse } from "./ThreadDecrementElicitationResponse"; -export type { ThreadDeleteParams } from "./ThreadDeleteParams"; -export type { ThreadDeleteResponse } from "./ThreadDeleteResponse"; -export type { ThreadDeletedNotification } from "./ThreadDeletedNotification"; -export type { ThreadExtra } from "./ThreadExtra"; -export type { ThreadForkParams } from "./ThreadForkParams"; -export type { ThreadForkResponse } from "./ThreadForkResponse"; -export type { ThreadGoal } from "./ThreadGoal"; -export type { ThreadGoalClearParams } from "./ThreadGoalClearParams"; -export type { ThreadGoalClearResponse } from "./ThreadGoalClearResponse"; -export type { ThreadGoalClearedNotification } from "./ThreadGoalClearedNotification"; -export type { ThreadGoalGetParams } from "./ThreadGoalGetParams"; -export type { ThreadGoalGetResponse } from "./ThreadGoalGetResponse"; -export type { ThreadGoalSetParams } from "./ThreadGoalSetParams"; -export type { ThreadGoalSetResponse } from "./ThreadGoalSetResponse"; -export type { ThreadGoalStatus } from "./ThreadGoalStatus"; -export type { ThreadGoalUpdatedNotification } from "./ThreadGoalUpdatedNotification"; -export type { ThreadHistoryMode } from "./ThreadHistoryMode"; -export type { ThreadIncrementElicitationParams } from "./ThreadIncrementElicitationParams"; -export type { ThreadIncrementElicitationResponse } from "./ThreadIncrementElicitationResponse"; -export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams"; -export type { ThreadInjectItemsResponse } from "./ThreadInjectItemsResponse"; -export type { ThreadItem } from "./ThreadItem"; -export type { ThreadItemEntry } from "./ThreadItemEntry"; -export type { ThreadItemsListParams } from "./ThreadItemsListParams"; -export type { ThreadItemsListResponse } from "./ThreadItemsListResponse"; -export type { ThreadListParams } from "./ThreadListParams"; -export type { ThreadListResponse } from "./ThreadListResponse"; -export type { ThreadLoadedListParams } from "./ThreadLoadedListParams"; -export type { ThreadLoadedListResponse } from "./ThreadLoadedListResponse"; -export type { ThreadMemoryModeSetParams } from "./ThreadMemoryModeSetParams"; -export type { ThreadMemoryModeSetResponse } from "./ThreadMemoryModeSetResponse"; -export type { ThreadMetadataGitInfoUpdateParams } from "./ThreadMetadataGitInfoUpdateParams"; -export type { ThreadMetadataUpdateParams } from "./ThreadMetadataUpdateParams"; -export type { ThreadMetadataUpdateResponse } from "./ThreadMetadataUpdateResponse"; -export type { ThreadNameUpdatedNotification } from "./ThreadNameUpdatedNotification"; -export type { ThreadProjectUpdatedNotification } from "./ThreadProjectUpdatedNotification"; -export type { ThreadQueueAddParams } from "./ThreadQueueAddParams"; -export type { ThreadQueueAddResponse } from "./ThreadQueueAddResponse"; -export type { ThreadQueueChangedNotification } from "./ThreadQueueChangedNotification"; -export type { ThreadQueueDeleteParams } from "./ThreadQueueDeleteParams"; -export type { ThreadQueueDeleteResponse } from "./ThreadQueueDeleteResponse"; -export type { ThreadQueueListParams } from "./ThreadQueueListParams"; -export type { ThreadQueueListResponse } from "./ThreadQueueListResponse"; -export type { ThreadQueueReorderParams } from "./ThreadQueueReorderParams"; -export type { ThreadQueueReorderResponse } from "./ThreadQueueReorderResponse"; -export type { ThreadQueueStartParams } from "./ThreadQueueStartParams"; -export type { ThreadQueueStartResponse } from "./ThreadQueueStartResponse"; -export type { ThreadQueueUpdateParams } from "./ThreadQueueUpdateParams"; -export type { ThreadQueueUpdateResponse } from "./ThreadQueueUpdateResponse"; -export type { ThreadReadParams } from "./ThreadReadParams"; -export type { ThreadReadResponse } from "./ThreadReadResponse"; -export type { ThreadRealtimeAppendAudioParams } from "./ThreadRealtimeAppendAudioParams"; -export type { ThreadRealtimeAppendAudioResponse } from "./ThreadRealtimeAppendAudioResponse"; -export type { ThreadRealtimeAppendSpeechParams } from "./ThreadRealtimeAppendSpeechParams"; -export type { ThreadRealtimeAppendSpeechResponse } from "./ThreadRealtimeAppendSpeechResponse"; -export type { ThreadRealtimeAppendTextParams } from "./ThreadRealtimeAppendTextParams"; -export type { ThreadRealtimeAppendTextResponse } from "./ThreadRealtimeAppendTextResponse"; -export type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk"; -export type { ThreadRealtimeBemItemPresentation } from "./ThreadRealtimeBemItemPresentation"; -export type { ThreadRealtimeClosedNotification } from "./ThreadRealtimeClosedNotification"; -export type { ThreadRealtimeErrorNotification } from "./ThreadRealtimeErrorNotification"; -export type { ThreadRealtimeInitialItem } from "./ThreadRealtimeInitialItem"; -export type { ThreadRealtimeItem } from "./ThreadRealtimeItem"; -export type { ThreadRealtimeItemAddedNotification } from "./ThreadRealtimeItemAddedNotification"; -export type { ThreadRealtimeItemCompletedNotification } from "./ThreadRealtimeItemCompletedNotification"; -export type { ThreadRealtimeItemStartedNotification } from "./ThreadRealtimeItemStartedNotification"; -export type { ThreadRealtimeItemTranscriptDeltaNotification } from "./ThreadRealtimeItemTranscriptDeltaNotification"; -export type { ThreadRealtimeListVoicesParams } from "./ThreadRealtimeListVoicesParams"; -export type { ThreadRealtimeListVoicesResponse } from "./ThreadRealtimeListVoicesResponse"; -export type { ThreadRealtimeOutputAudioDeltaNotification } from "./ThreadRealtimeOutputAudioDeltaNotification"; -export type { ThreadRealtimeSdpNotification } from "./ThreadRealtimeSdpNotification"; -export type { ThreadRealtimeSessionOutcome } from "./ThreadRealtimeSessionOutcome"; -export type { ThreadRealtimeStartParams } from "./ThreadRealtimeStartParams"; -export type { ThreadRealtimeStartResponse } from "./ThreadRealtimeStartResponse"; -export type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTransport"; -export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification"; -export type { ThreadRealtimeStopParams } from "./ThreadRealtimeStopParams"; -export type { ThreadRealtimeStopResponse } from "./ThreadRealtimeStopResponse"; -export type { ThreadRealtimeTranscriptDeltaNotification } from "./ThreadRealtimeTranscriptDeltaNotification"; -export type { ThreadRealtimeTranscriptDoneNotification } from "./ThreadRealtimeTranscriptDoneNotification"; -export type { ThreadRealtimeTranscriptRole } from "./ThreadRealtimeTranscriptRole"; -export type { ThreadResumeInitialTurnsPageParams } from "./ThreadResumeInitialTurnsPageParams"; -export type { ThreadResumeParams } from "./ThreadResumeParams"; -export type { ThreadResumeResponse } from "./ThreadResumeResponse"; -export type { ThreadRevertParams } from "./ThreadRevertParams"; -export type { ThreadRevertResponse } from "./ThreadRevertResponse"; -export type { ThreadRevertedNotification } from "./ThreadRevertedNotification"; -export type { ThreadRollbackParams } from "./ThreadRollbackParams"; -export type { ThreadRollbackResponse } from "./ThreadRollbackResponse"; -export type { ThreadSearchOccurrence } from "./ThreadSearchOccurrence"; -export type { ThreadSearchOccurrencesParams } from "./ThreadSearchOccurrencesParams"; -export type { ThreadSearchOccurrencesResponse } from "./ThreadSearchOccurrencesResponse"; -export type { ThreadSearchParams } from "./ThreadSearchParams"; -export type { ThreadSearchResponse } from "./ThreadSearchResponse"; -export type { ThreadSearchResult } from "./ThreadSearchResult"; -export type { ThreadSearchSortKey } from "./ThreadSearchSortKey"; -export type { ThreadSearchTextRange } from "./ThreadSearchTextRange"; -export type { ThreadSection } from "./ThreadSection"; -export type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; -export type { ThreadSectionCreateParams } from "./ThreadSectionCreateParams"; -export type { ThreadSectionCreateResponse } from "./ThreadSectionCreateResponse"; -export type { ThreadSectionDeleteParams } from "./ThreadSectionDeleteParams"; -export type { ThreadSectionDeleteResponse } from "./ThreadSectionDeleteResponse"; -export type { ThreadSectionListParams } from "./ThreadSectionListParams"; -export type { ThreadSectionListResponse } from "./ThreadSectionListResponse"; -export type { ThreadSectionMoveParams } from "./ThreadSectionMoveParams"; -export type { ThreadSectionMoveResponse } from "./ThreadSectionMoveResponse"; -export type { ThreadSectionUpdateParams } from "./ThreadSectionUpdateParams"; -export type { ThreadSectionUpdateResponse } from "./ThreadSectionUpdateResponse"; -export type { ThreadSetNameParams } from "./ThreadSetNameParams"; -export type { ThreadSetNameResponse } from "./ThreadSetNameResponse"; -export type { ThreadSettings } from "./ThreadSettings"; -export type { ThreadSettingsUpdateParams } from "./ThreadSettingsUpdateParams"; -export type { ThreadSettingsUpdateResponse } from "./ThreadSettingsUpdateResponse"; -export type { ThreadSettingsUpdatedNotification } from "./ThreadSettingsUpdatedNotification"; -export type { ThreadShellCommandParams } from "./ThreadShellCommandParams"; -export type { ThreadShellCommandResponse } from "./ThreadShellCommandResponse"; -export type { ThreadSortKey } from "./ThreadSortKey"; -export type { ThreadSource } from "./ThreadSource"; -export type { ThreadSourceKind } from "./ThreadSourceKind"; -export type { ThreadStartParams } from "./ThreadStartParams"; -export type { ThreadStartResponse } from "./ThreadStartResponse"; -export type { ThreadStartSource } from "./ThreadStartSource"; -export type { ThreadStartedNotification } from "./ThreadStartedNotification"; -export type { ThreadStatus } from "./ThreadStatus"; -export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification"; -export type { ThreadTimelineEntry } from "./ThreadTimelineEntry"; -export type { ThreadTimelineListParams } from "./ThreadTimelineListParams"; -export type { ThreadTimelineListResponse } from "./ThreadTimelineListResponse"; -export type { ThreadTokenUsage } from "./ThreadTokenUsage"; -export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification"; -export type { ThreadTurnsListParams } from "./ThreadTurnsListParams"; -export type { ThreadTurnsListResponse } from "./ThreadTurnsListResponse"; -export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams"; -export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse"; -export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification"; -export type { ThreadUnsubscribeParams } from "./ThreadUnsubscribeParams"; -export type { ThreadUnsubscribeResponse } from "./ThreadUnsubscribeResponse"; -export type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus"; -export type { ThreadUsage } from "./ThreadUsage"; -export type { ThreadUsageBreakdownGroup } from "./ThreadUsageBreakdownGroup"; -export type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; -export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer"; -export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption"; -export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams"; -export type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion"; -export type { ToolRequestUserInputResponse } from "./ToolRequestUserInputResponse"; -export type { ToolsV2 } from "./ToolsV2"; -export type { Turn } from "./Turn"; -export type { TurnCompletedNotification } from "./TurnCompletedNotification"; -export type { TurnDiffUpdatedNotification } from "./TurnDiffUpdatedNotification"; -export type { TurnEnvironmentParams } from "./TurnEnvironmentParams"; -export type { TurnError } from "./TurnError"; -export type { TurnInterruptParams } from "./TurnInterruptParams"; -export type { TurnInterruptResponse } from "./TurnInterruptResponse"; -export type { TurnItemsView } from "./TurnItemsView"; -export type { TurnModerationMetadataNotification } from "./TurnModerationMetadataNotification"; -export type { TurnPlanStep } from "./TurnPlanStep"; -export type { TurnPlanStepStatus } from "./TurnPlanStepStatus"; -export type { TurnPlanUpdatedNotification } from "./TurnPlanUpdatedNotification"; -export type { TurnStartParams } from "./TurnStartParams"; -export type { TurnStartResponse } from "./TurnStartResponse"; -export type { TurnStartedNotification } from "./TurnStartedNotification"; -export type { TurnStatus } from "./TurnStatus"; -export type { TurnSteerParams } from "./TurnSteerParams"; -export type { TurnSteerResponse } from "./TurnSteerResponse"; -export type { TurnsPage } from "./TurnsPage"; -export type { UserInput } from "./UserInput"; -export type { WarningNotification } from "./WarningNotification"; -export type { WebSearchAction } from "./WebSearchAction"; -export type { WindowsSandboxReadiness } from "./WindowsSandboxReadiness"; -export type { WindowsSandboxReadinessResponse } from "./WindowsSandboxReadinessResponse"; -export type { WindowsSandboxSetupCompletedNotification } from "./WindowsSandboxSetupCompletedNotification"; -export type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type { WindowsSandboxSetupStartParams } from "./WindowsSandboxSetupStartParams"; -export type { WindowsSandboxSetupStartResponse } from "./WindowsSandboxSetupStartResponse"; -export type { WindowsWorldWritableWarningNotification } from "./WindowsWorldWritableWarningNotification"; -export type { WorkspaceMessage } from "./WorkspaceMessage"; -export type { WorkspaceMessageType } from "./WorkspaceMessageType"; -export type { WriteStatus } from "./WriteStatus"; diff --git a/src/runtimes/provider-options.ts b/src/runtimes/provider-options.ts index eb210a8..059807e 100644 --- a/src/runtimes/provider-options.ts +++ b/src/runtimes/provider-options.ts @@ -9,19 +9,18 @@ function deepMergeRecords( base: Record, providerOptions: JsonObject, ): Record { - const result: Record = { ...base }; - - for (const [key, value] of Object.entries(providerOptions)) { - const current = result[key]; - - if (isMergeableRecord(current) && isJsonObject(value)) { - result[key] = deepMergeRecords(current, value); - } else { - result[key] = structuredClone(value); - } - } - - return result; + return Object.fromEntries([ + ...Object.entries(base), + ...Object.entries(providerOptions).map(([key, value]) => { + const current = Object.hasOwn(base, key) ? base[key] : undefined; + return [ + key, + isMergeableRecord(current) && isJsonObject(value) + ? deepMergeRecords(current, value) + : structuredClone(value), + ]; + }), + ]); } export function mergeProviderOptions(base: T, providerOptions: JsonObject): T { diff --git a/tests/acp-client-request-handler.test.ts b/tests/acp-client-request-handler.test.ts index 2fd61b5..fbbb461 100644 --- a/tests/acp-client-request-handler.test.ts +++ b/tests/acp-client-request-handler.test.ts @@ -13,18 +13,23 @@ import type { DriverEventInput } from "../src/protocol/events"; import { AcpClientRequestHandler } from "../src/runtimes/acp/acp-client-request-handler"; import { AcpAssistantTranscriptState } from "../src/runtimes/acp/acp-assistant-transcript-state"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; +import { beginAcpTranscript } from "./acp-test-helpers"; + +const BASE_HANDLER_OPTIONS = { + allowedRoots: [], + cwd: "/workspace", + env: {}, + isCancelling: () => false, + nativeSessionId: () => "native-session-1", + onUpdateFailure: () => {}, +} satisfies Omit[0], "push" | "turnEvents">; describe("ACP client request handler", () => { test("rejects permission requests outside an active turn", async () => { let permissionRequests = 0; let pushes = 0; const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async () => { pushes += 1; }, @@ -55,19 +60,10 @@ describe("ACP client request handler", () => { }); test("closes permission ingress before a turn terminal and reopens it for the next turn", async () => { - const turnEvents = new AcpAssistantTranscriptState(); + const turnEvents = beginAcpTranscript(); let permissionRequests = 0; - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async () => {}, turnEvents, }); @@ -100,12 +96,7 @@ describe("ACP client request handler", () => { test("suppresses turn-scoped session updates before a turn is active", async () => { const pushedReasons: string[] = []; const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async (_context, reason, _events) => { pushedReasons.push(reason); }, @@ -139,12 +130,7 @@ describe("ACP client request handler", () => { test("discards deferred updates and permits a later gate", async () => { const pushedReasons: string[] = []; const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async (_context, reason) => { pushedReasons.push(reason); }, @@ -173,12 +159,9 @@ describe("ACP client request handler", () => { test("passes the runtime environment to terminal child processes", async () => { const handler = new AcpClientRequestHandler({ - allowedRoots: [], + ...BASE_HANDLER_OPTIONS, cwd: process.cwd(), env: { PATH: "/artifact/bin:/runtime/bin" }, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, push: async () => {}, turnEvents: new AcpAssistantTranscriptState(), }); @@ -207,18 +190,9 @@ describe("ACP client request handler", () => { test("serializes official SDK notifications and drains scoped suppression", async () => { const gate = Promise.withResolvers(); const pushedReasons: string[] = []; - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async (_context, reason) => { pushedReasons.push(reason); if (pushedReasons.length === 1) { @@ -273,18 +247,10 @@ describe("ACP client request handler", () => { const firstAdmitted = Promise.withResolvers(); const failures: Error[] = []; const pending: Promise[] = []; - const turnEvents = new AcpAssistantTranscriptState(); + const turnEvents = beginAcpTranscript(); let pushes = 0; - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", + ...BASE_HANDLER_OPTIONS, onUpdateFailure: (error) => failures.push(error), push: async () => { pushes += 1; @@ -326,16 +292,11 @@ describe("ACP client request handler", () => { test("admits replay and completed history but rejects genuinely open retained state", async () => { const createHandler = () => { const failures: Error[] = []; - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never }); + const turnEvents = beginAcpTranscript(); return { failures, handler: new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", + ...BASE_HANDLER_OPTIONS, onUpdateFailure: (error) => failures.push(error), push: async () => {}, turnEvents, @@ -425,14 +386,9 @@ describe("ACP client request handler", () => { test("routes permission turn-state overflow through provider fatal cleanup", async () => { const failures: Error[] = []; - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ messageId: "message-1" as never, runId: "run-1" as never }); + const turnEvents = beginAcpTranscript(); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", + ...BASE_HANDLER_OPTIONS, onUpdateFailure: (error) => failures.push(error), push: async () => {}, turnEvents, @@ -458,17 +414,9 @@ describe("ACP client request handler", () => { test("fails every queued update after the first commit failure", async () => { const failures: Error[] = []; let pushes = 0; - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", + ...BASE_HANDLER_OPTIONS, onUpdateFailure: (error) => failures.push(error), push: async () => { pushes += 1; @@ -505,21 +453,12 @@ describe("ACP client request handler", () => { const firstPublishing = Promise.withResolvers(); const rejectFirst = Promise.withResolvers(); const attempts: unknown[] = []; - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const createHandler = ( push: ConstructorParameters[0]["push"], ) => new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push, turnEvents, }); @@ -565,18 +504,9 @@ describe("ACP client request handler", () => { const permissionPublishing = Promise.withResolvers(); const rejectPermission = Promise.withResolvers(); const updateAttempts: unknown[] = []; - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async (_context, reason, events) => { if (reason === "driver.acp.permission.tool") { permissionPublishing.resolve(); @@ -632,11 +562,7 @@ describe("ACP client request handler", () => { const attempts: DriverEventInput[][] = []; let attempt = 0; let sequence = 0; - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const context = { logger: { debug: () => {} }, ports: { @@ -671,12 +597,7 @@ describe("ACP client request handler", () => { } as never; const publisher = new DriverEventPublisher("acp-fallback", () => "native-session-1"); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: (pushContext, reason, events) => publisher.push(pushContext, reason, events), turnEvents, }); @@ -720,11 +641,7 @@ describe("ACP client request handler", () => { const attempts: DriverEventInput[][] = []; let reject = true; let sequence = 0; - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const context = { logger: { debug: () => {} }, ports: { @@ -753,12 +670,7 @@ describe("ACP client request handler", () => { const publisher = new DriverEventPublisher("acp-fallback", () => "native-session-1"); const createHandler = () => new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: (pushContext, reason, events) => publisher.push(pushContext, reason, events), turnEvents, }); @@ -793,22 +705,14 @@ describe("ACP client request handler", () => { async (boundary) => { const blocked = Promise.withResolvers(); const release = Promise.withResolvers(); - const turnEvents = new AcpAssistantTranscriptState(); + const turnEvents = beginAcpTranscript(); let cancelling = false; let permissionRequests = 0; - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); const blockedReason = boundary === "update drain" ? "driver.acp.session.update" : "driver.acp.permission.tool"; const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, + ...BASE_HANDLER_OPTIONS, isCancelling: () => cancelling, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, push: async (_context, reason) => { if (reason === blockedReason) { blocked.resolve(); @@ -857,19 +761,10 @@ describe("ACP client request handler", () => { test("rejects a permission when its turn ends during the update drain", async () => { const updatePublishing = Promise.withResolvers(); const releaseUpdate = Promise.withResolvers(); - const turnEvents = new AcpAssistantTranscriptState(); + const turnEvents = beginAcpTranscript(); let permissionRequests = 0; - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async (_context, reason) => { if (reason === "driver.acp.session.update") { updatePublishing.resolve(); @@ -915,18 +810,9 @@ describe("ACP client request handler", () => { test("leaves permission lifecycle events to the host port and keeps typed RPC IDs distinct", async () => { const eventKinds: string[] = []; const requests: DriverPermissionRequest[] = []; - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async (_context, _reason, events) => { eventKinds.push(...events.map((event) => event.kind)); }, @@ -973,18 +859,9 @@ describe("ACP client request handler", () => { ] as const)( "maps a %s host decision to a %s option without widening scope", async (decision, optionKind, optionId, selectable) => { - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async () => {}, turnEvents, }); @@ -1009,18 +886,9 @@ describe("ACP client request handler", () => { test("returns cancelled when the SDK aborts a pending permission request", async () => { const entered = Promise.withResolvers(); const controller = new AbortController(); - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async () => {}, turnEvents, }); @@ -1056,19 +924,10 @@ describe("ACP client request handler", () => { const toolPublishing = Promise.withResolvers(); const releaseTool = Promise.withResolvers(); const controller = new AbortController(); - const turnEvents = new AcpAssistantTranscriptState(); + const turnEvents = beginAcpTranscript(); let permissionRequests = 0; - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async (_context, reason) => { if (reason === "driver.acp.permission.tool") { toolPublishing.resolve(); @@ -1121,18 +980,9 @@ describe("ACP client request handler", () => { "resolved", new Error("permission transport unavailable"), ); - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async () => {}, turnEvents, }); @@ -1189,18 +1039,9 @@ describe("ACP client request handler", () => { const broker = new DriverPermissionBroker(() => null, { eventDeliveryTimeoutMs: 1_000, }); - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: activeRunId as never, - }); + const turnEvents = beginAcpTranscript({ runId: activeRunId as never }); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async () => {}, turnEvents, }); @@ -1245,18 +1086,9 @@ describe("ACP client request handler", () => { test("closes update ingress and drains accepted work before stopping", async () => { const blocked = Promise.withResolvers(); const release = Promise.withResolvers(); - const turnEvents = new AcpAssistantTranscriptState(); - turnEvents.begin({ - messageId: "message-1" as never, - runId: "run-1" as never, - }); + const turnEvents = beginAcpTranscript(); const handler = new AcpClientRequestHandler({ - allowedRoots: [], - cwd: "/workspace", - env: {}, - isCancelling: () => false, - nativeSessionId: () => "native-session-1", - onUpdateFailure: () => {}, + ...BASE_HANDLER_OPTIONS, push: async (_context, reason) => { if (reason === "driver.acp.session.update") { blocked.resolve(); diff --git a/tests/acp-configuration.test.ts b/tests/acp-configuration.test.ts index bf27a83..d264b2f 100644 --- a/tests/acp-configuration.test.ts +++ b/tests/acp-configuration.test.ts @@ -1,10 +1,6 @@ import { describe, expect, test } from "bun:test"; import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { createDisabledLogger } from "../src/observability"; import { ACP_PROTOCOL_VERSION, appendOpenCodeInstruction, @@ -17,8 +13,7 @@ import { supportsSessionClose, supportsSessionResume, } from "../src/runtimes/acp/acp-configuration"; -import { AcpDriverBackend, limitAcpInput } from "../src/runtimes/acp/acp-driver-backend"; -import { createAgentDriverContext } from "../src/core/agent-driver-backend"; +import { limitAcpInput } from "../src/runtimes/acp/acp-driver-backend"; import { bootPayload } from "./driver-runtime-boundary-fixtures"; function createInitializeResult(protocolVersion: number | string | null) { @@ -294,41 +289,4 @@ describe("ACP runtime configuration", () => { expect(env["HTTPS_PROXY"]).toBe("http://explicit-proxy:7897"); expect(env["NO_PROXY"]).toBe("metadata.google.internal"); }); - - test("requires host integration snapshot before starting", async () => { - const root = await mkdtemp(join(tmpdir(), "driver-acp-configuration-")); - const payload = { - ...bootPayload, - execution: { - ...bootPayload.execution, - session: { - ...bootPayload.execution.session, - cwd: root, - homePath: join(root, "home"), - sharedRootPath: root, - }, - }, - }; - const backend = new AcpDriverBackend(payload); - const context = createAgentDriverContext({ - eventSink: { - commandUpdate: async () => {}, - currentRunId: () => null, - pushEvents: async () => ({ accepted: [] }), - }, - logger: createDisabledLogger(), - payload, - permission: { - request: async () => "reject_once", - }, - }); - - try { - await expect(backend.start(context, new AbortController().signal)).rejects.toThrow( - "ACP fallback requires a host integration snapshot.", - ); - } finally { - await rm(root, { force: true, recursive: true }); - } - }); }); diff --git a/tests/acp-driver-backend.test.ts b/tests/acp-driver-backend.test.ts index ce97b7f..6dd997e 100644 --- a/tests/acp-driver-backend.test.ts +++ b/tests/acp-driver-backend.test.ts @@ -13,7 +13,6 @@ import type { DriverRuntimeEventPort } from "../src/core/driver-runtime-io"; import { createDisabledLogger } from "../src/observability"; import type { AgentDriverPermissionPort } from "../src/host-ports"; import type { DriverBootPayload } from "../src/protocol/boot"; -import { createDriverHostIntegrationSnapshotFromBootExecution } from "../src/protocol/host-integration"; import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; @@ -23,6 +22,7 @@ import { AcpClientRequestHandler } from "../src/runtimes/acp/acp-client-request- import { AcpTurnController } from "../src/runtimes/acp/acp-turn-controller"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { settlePromiseWithTimeout } from "../src/utils/async"; +import { waitForAcpTestCondition } from "./acp-test-helpers"; import { driverBootPayload, DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; const FAKE_AGENT = String.raw` @@ -566,9 +566,6 @@ async function createHarness( payload, permission: { request: options.permission ?? (async () => "reject_once") }, ports: { - hostIntegration: { - snapshot: async () => createDriverHostIntegrationSnapshotFromBootExecution(boot.execution), - }, skill: { materialize: async () => [] }, }, }); @@ -659,7 +656,6 @@ describe("ACP driver backend lifecycle", () => { } satisfies DriverBootPayload; const payload = createDriverStartInputFromBootPayload(boot); const logger = createDisabledLogger(); - let hostSnapshots = 0; let materializations = 0; const context = createAgentDriverContext({ eventSink: { @@ -670,12 +666,6 @@ describe("ACP driver backend lifecycle", () => { payload, permission: { request: async () => "reject_once" }, ports: { - hostIntegration: { - snapshot: async () => { - hostSnapshots += 1; - return createDriverHostIntegrationSnapshotFromBootExecution(boot.execution); - }, - }, skill: { materialize: async () => { materializations += 1; @@ -690,7 +680,6 @@ describe("ACP driver backend lifecycle", () => { await expect(backend.start(context, new AbortController().signal)).rejects.toMatchObject({ code: "ENOENT", }); - expect(hostSnapshots).toBe(0); expect(materializations).toBe(0); await expect( Promise.all([ @@ -1052,21 +1041,17 @@ describe("ACP driver backend lifecycle", () => { DRIVER_TEST_IDS.runId as RunId, ); void input.catch(() => {}); - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.methods()).includes("session/prompt")) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/prompt"), + "ACP session/prompt request", + ); const gate = harness.blockNext("run.cancel.requested"); const cancel = harness.backend.cancelActiveTurn(harness.context, "test cancellation"); await gate.entered; - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.methods()).includes("session/cancel")) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/cancel"), + "ACP session/cancel request", + ); expect(await harness.methods()).toContain("session/cancel"); expect( @@ -1101,22 +1086,18 @@ describe("ACP driver backend lifecycle", () => { DRIVER_TEST_IDS.runId as RunId, ); void input.catch(() => {}); - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.methods()).includes("session/prompt")) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/prompt"), + "ACP session/prompt request", + ); await expect( harness.backend.cancelActiveTurn(harness.context, "test cancellation"), ).resolves.toBeUndefined(); - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.methods()).includes("session/resume")) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/resume"), + "ACP session/resume request", + ); expect(await harness.methods()).toContain("session/resume"); expect(harness.events).not.toContainEqual( @@ -1174,12 +1155,10 @@ describe("ACP driver backend lifecycle", () => { DRIVER_TEST_IDS.runId as RunId, ); void input.catch(() => {}); - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.methods()).includes("session/prompt")) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/prompt"), + "ACP session/prompt request", + ); await expect( harness.backend.cancelActiveTurn(harness.context, "test cancellation"), @@ -1204,12 +1183,10 @@ describe("ACP driver backend lifecycle", () => { DRIVER_TEST_IDS.runId as RunId, ); void input.catch(() => {}); - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.methods()).includes("session/prompt")) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/prompt"), + "ACP session/prompt request", + ); await harness.backend.cancelActiveTurn(harness.context, "test cancellation"); await expect(input).rejects.toThrow("cancelled"); @@ -1263,7 +1240,6 @@ describe("ACP driver backend lifecycle", () => { DRIVER_TEST_IDS.runId as RunId, connection, "native-session-1", - createDriverHostIntegrationSnapshotFromBootExecution(driverBootPayload.execution), clientRequests, ); void input.catch(() => {}); @@ -1318,7 +1294,6 @@ describe("ACP driver backend lifecycle", () => { DRIVER_TEST_IDS.runId as RunId, connection, "native-session-1", - createDriverHostIntegrationSnapshotFromBootExecution(driverBootPayload.execution), clientRequests, ); void input.catch(() => {}); @@ -1381,7 +1356,6 @@ describe("ACP driver backend lifecycle", () => { DRIVER_TEST_IDS.runId as RunId, connection, "native-session-1", - createDriverHostIntegrationSnapshotFromBootExecution(driverBootPayload.execution), clientRequests, ), ).rejects.toThrow("terminal settlement rejected"); @@ -1435,7 +1409,6 @@ describe("ACP driver backend lifecycle", () => { DRIVER_TEST_IDS.runId as RunId, connection, "native-session-1", - createDriverHostIntegrationSnapshotFromBootExecution(driverBootPayload.execution), clientRequests, ); void input.catch(() => {}); @@ -1708,12 +1681,10 @@ describe("ACP driver backend lifecycle", () => { const requestId = await requestedDelivered.promise; expect(broker.resolve(requestId, "allow_once")).toBe(true); await permissionSettled.promise; - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.responses()).length > 0) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.responses()).length > 0, + "ACP nested permission response", + ); expect(await harness.responses()).toHaveLength(1); const cancel = harness.backend.cancelActiveTurn(harness.context, "test cancellation"); @@ -1939,23 +1910,19 @@ describe("ACP driver backend lifecycle", () => { ); void input.catch(() => {}); - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.methods()).includes("terminal/wait_for_exit")) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("terminal/wait_for_exit"), + "ACP terminal/wait_for_exit request", + ); expect(await harness.methods()).toContain("terminal/wait_for_exit"); await harness.backend.cancelActiveTurn(harness.context, "test cancellation"); await expect(input).rejects.toThrow("cancelled"); - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.responses()).length > 0) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.responses()).length > 0, + "ACP nested terminal response", + ); expect(await harness.responses()).toContainEqual( expect.objectContaining({ @@ -1979,12 +1946,10 @@ describe("ACP driver backend lifecycle", () => { ); void input.catch(() => {}); - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.methods()).includes("session/prompt")) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/prompt"), + "ACP session/prompt request", + ); expect(await harness.methods()).toContain("session/prompt"); await expect( @@ -2062,12 +2027,10 @@ describe("ACP driver backend lifecycle", () => { DRIVER_TEST_IDS.runId as RunId, ); void input.catch(() => {}); - for (let attempt = 0; attempt < 100; attempt += 1) { - if ((await harness.methods()).includes("session/prompt")) { - break; - } - await Bun.sleep(5); - } + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/prompt"), + "ACP session/prompt request", + ); const gate = harness.blockNext("usage.updated"); const stop = harness.backend.stop( diff --git a/tests/acp-event-translator-lifecycle.test.ts b/tests/acp-event-translator-lifecycle.test.ts index a756eaf..237f6d6 100644 --- a/tests/acp-event-translator-lifecycle.test.ts +++ b/tests/acp-event-translator-lifecycle.test.ts @@ -1,10 +1,6 @@ import { describe, expect, test } from "bun:test"; import { RequestError } from "@agentclientprotocol/sdk"; -import { - zAgentResponse, - zPromptRequest, - zSessionNotification, -} from "../node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js"; +import type { PromptRequest, SessionNotification } from "@agentclientprotocol/sdk"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; @@ -28,6 +24,7 @@ import { driverBootPayload, driverStartInput, } from "./driver-boot-payload-fixture"; +import { beginAcpTranscript } from "./acp-test-helpers"; const RUN_ID = "run-1" as RunId; const SECOND_RUN_ID = "run-2" as RunId; @@ -36,6 +33,10 @@ function eventKinds(events: readonly DriverEventInput[]): string[] { return events.map((event) => event.kind); } +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + function eventPayload(event: DriverEventInput): Record { expect(event.payload).toBeObject(); return event.payload as Record; @@ -72,17 +73,16 @@ describe("ACP runtime event translation", () => { const decoded = JSON.parse( await new Response(limitAcpInput(new Blob([wire]).stream())).text(), ) as unknown; - const response = zAgentResponse.parse(decoded); + if (!isRecord(decoded) || decoded["id"] !== 1 || decoded["jsonrpc"] !== "2.0") { + throw new Error("Expected an official ACP response envelope."); + } + const error = decoded["error"]; - if (!("error" in response)) { + if (!isRecord(error) || error["code"] !== -32_603 || typeof error["message"] !== "string") { throw new Error("Expected an official ACP error response."); } - const requestError = new RequestError( - response.error.code, - response.error.message, - response.error.data, - ); + const requestError = new RequestError(error["code"], error["message"], error["data"]); const state = new AcpAssistantTranscriptState(); state.begin({ messageId: "message-1", @@ -184,27 +184,27 @@ describe("ACP runtime event translation", () => { }); test("bounds official ACP prompt text before provider dispatch", async () => { - const parseText = (text: string): string => { - const prompt = zPromptRequest.parse({ + const promptText = (text: string): string => { + const prompt = { prompt: [{ text, type: "text" }], sessionId: "native-session-1", - }).prompt[0]; + } satisfies PromptRequest; - return prompt?.type === "text" ? prompt.text : ""; + return prompt.prompt[0]!.text; }; expect(() => toPromptStartEvents({ messageId: "message-1", runId: DRIVER_TEST_IDS.runId, - text: parseText("x".repeat(1_100_000)), + text: promptText("x".repeat(1_100_000)), }), ).toThrow("ACP message.added event exceeds 524288 UTF-8 bytes"); const events = toPromptStartEvents({ messageId: "message-1", runId: DRIVER_TEST_IDS.runId, - text: parseText("x".repeat(500_000)), + text: promptText("x".repeat(500_000)), }); const store = createCmaMemoryStore({ sessions: [{ id: DRIVER_TEST_IDS.sessionId }] }); let recordCount = 0; @@ -237,35 +237,39 @@ describe("ACP runtime event translation", () => { }); for (const sessionUpdate of ["agent_message_chunk", "agent_thought_chunk"] as const) { - const notification = zSessionNotification.parse({ + const notification = { sessionId: "native-session-1", update: { content: { text: "", type: "text" }, sessionUpdate }, - }); + } satisfies SessionNotification; expect(state.translateUpdate(notification)).toEqual([]); } - const message = state.translateUpdate( - zSessionNotification.parse({ - sessionId: "native-session-1", - update: { - content: { text: "ok", type: "text" }, - sessionUpdate: "agent_message_chunk", - }, - }), - ); + const message = state.translateUpdate({ + sessionId: "native-session-1", + update: { + content: { text: "ok", type: "text" }, + sessionUpdate: "agent_message_chunk", + }, + } satisfies SessionNotification); expect(eventKinds(message)).toEqual(["message.started", "message.delta"]); expect(message[1]?.sourceEventId).toBe(`acp:${DRIVER_TEST_IDS.runId}:agent-message:1`); for (const sessionUpdate of ["tool_call", "tool_call_update"] as const) { - const notification = zSessionNotification.parse({ - sessionId: "native-session-1", - update: { - sessionUpdate, - title: "", - toolCallId: sessionUpdate, - ...(sessionUpdate === "tool_call" ? { kind: "execute" } : {}), - }, - }); + const notification = + sessionUpdate === "tool_call" + ? ({ + sessionId: "native-session-1", + update: { + kind: "execute", + sessionUpdate, + title: "", + toolCallId: sessionUpdate, + }, + } satisfies SessionNotification) + : ({ + sessionId: "native-session-1", + update: { sessionUpdate, title: "", toolCallId: sessionUpdate }, + } satisfies SessionNotification); const events = state.translateUpdate(notification); const update = requireEvent(events, "tool.call.updated"); @@ -384,8 +388,7 @@ describe("ACP runtime event translation", () => { }); test("evicts bounded settled assistant IDs without suppressing the oldest message", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); const first = state.translateUpdate({ update: { @@ -432,10 +435,9 @@ describe("ACP runtime event translation", () => { }); test("keeps the maximum admitted assistant text inside the shared terminal budget", () => { - const state = new AcpAssistantTranscriptState(); + const state = beginAcpTranscript(); const content = "x".repeat(8 * 1_024); - state.begin({ messageId: "message-1", runId: RUN_ID }); for (let index = 0; index < 47; index += 1) { state.translateUpdate({ update: { @@ -454,8 +456,7 @@ describe("ACP runtime event translation", () => { }); test("keeps the maximum retained item closures inside the shared terminal budget", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); for (let index = 0; index < 509; index += 1) { state.translateUpdate({ @@ -482,9 +483,8 @@ describe("ACP runtime event translation", () => { }); test("keeps prompt usage extensions out of the lossless terminal budget", () => { - const state = new AcpAssistantTranscriptState(); + const state = beginAcpTranscript(); const fallbackToolId = "request-".repeat(40_000); - state.begin({ messageId: "message-1", runId: RUN_ID }); state.translatePermission({ params: { options: [{ kind: "allow_once", name: "Allow", optionId: "allow" }], @@ -617,12 +617,7 @@ describe("ACP runtime event translation", () => { }); test("maps ACP turn updates onto canonical runtime events with one tool lifecycle", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript(); const events = [ ...state.translateUpdate({ @@ -683,8 +678,7 @@ describe("ACP runtime event translation", () => { }); test("projects an execute tool with a nonzero raw exit as failed", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); state.translateUpdate({ update: { kind: "execute", @@ -719,8 +713,7 @@ describe("ACP runtime event translation", () => { }); test("deduplicates identical ACP tool content and fails closed on oversized output", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); state.translateUpdate({ update: { kind: "execute", @@ -758,8 +751,7 @@ describe("ACP runtime event translation", () => { }); test("fails closed on an oversized permission tool payload", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); expect(() => state.translatePermission({ @@ -789,8 +781,7 @@ describe("ACP runtime event translation", () => { }); test("accounts for a permission request ID retained as the fallback tool ID", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); expect(() => state.translatePermission({ @@ -816,8 +807,7 @@ describe("ACP runtime event translation", () => { }); test("keeps a nonzero execute exit across partial updates", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); state.translateUpdate({ update: { kind: "execute", @@ -858,8 +848,7 @@ describe("ACP runtime event translation", () => { }); test("does not reset tool identity fields omitted by a partial update", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); const started = state.translateUpdate({ update: { @@ -903,8 +892,7 @@ describe("ACP runtime event translation", () => { ] as const)( "keeps a terminal tool completed while merging a later %s patch", (_name, patch, expected) => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); const initial = state.translateUpdate({ update: { kind: "shell", @@ -949,8 +937,7 @@ describe("ACP runtime event translation", () => { ); test("evicts completed replay history only after projecting its late update", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); for (const toolCallId of ["tool-0", "tool-1"]) { state.translateUpdate({ @@ -991,8 +978,7 @@ describe("ACP runtime event translation", () => { ] as const)( "keeps the first %s status across a late %s update %s", (initialStatus, lateStatus, contentCase) => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); state.translateUpdate({ update: { sessionUpdate: "tool_call", @@ -1027,8 +1013,7 @@ describe("ACP runtime event translation", () => { ); test("emits an empty plan as a full replacement", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript(); expect( state.translateUpdate({ diff --git a/tests/acp-event-translator-session-update.test.ts b/tests/acp-event-translator-session-update.test.ts index 12fdc8c..5a4e257 100644 --- a/tests/acp-event-translator-session-update.test.ts +++ b/tests/acp-event-translator-session-update.test.ts @@ -8,6 +8,7 @@ import { toInitializeEvents, toSessionReadyEvents, } from "../src/runtimes/acp/acp-session-events"; +import { beginAcpTranscript } from "./acp-test-helpers"; const RUN_ID = "run-1" as RunId; @@ -42,12 +43,7 @@ function requireEvent(events: readonly DriverEventInput[], kind: string): Driver describe("ACP runtime event translation", () => { test("starts permission tool calls through the turn event state", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); const translation = state.translatePermission({ params: { @@ -76,12 +72,7 @@ describe("ACP runtime event translation", () => { }); test("closes unfinished tool calls when a turn completes", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); const events = [ ...state.translateUpdate({ @@ -115,12 +106,7 @@ describe("ACP runtime event translation", () => { }); test("fails open items when max turn requests stops a prompt", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); const events = [ ...state.translateUpdate({ @@ -164,9 +150,7 @@ describe("ACP runtime event translation", () => { }); test("cancels every open item when the prompt is cancelled", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript({ runId: RUN_ID }); const events = [ ...state.translateUpdate({ update: { @@ -211,12 +195,7 @@ describe("ACP runtime event translation", () => { }); test("fails an empty end turn instead of reporting a blank completed run", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); const events = state.completePrompt("end_turn", { inputTokens: 0, @@ -236,12 +215,7 @@ describe("ACP runtime event translation", () => { }); test("ignores ACP user message echo chunks because driver input is the source of truth", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); expect( state.translateUpdate({ @@ -257,12 +231,7 @@ describe("ACP runtime event translation", () => { }); test("promotes message-scoped thought-only ACP output into an assistant message", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); const streamed = [ ...state.translateUpdate({ @@ -313,12 +282,7 @@ describe("ACP runtime event translation", () => { }); test("treats a different native thought as the final assistant message boundary", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); const progressText = "PROGRESS:正在整理资料。"; const finalText = "FINAL:中文表格与结论均已完成。"; @@ -375,12 +339,7 @@ describe("ACP runtime event translation", () => { }); test("fails closed when an anonymous thought follows identified progress", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); const events = [ ...state.translateUpdate({ @@ -420,12 +379,7 @@ describe("ACP runtime event translation", () => { }); test("closes open stream items before a failed turn event", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); const events = [ ...state.translateUpdate({ @@ -649,8 +603,7 @@ describe("ACP runtime event translation", () => { "ACP auth.session.updated event exceeds 524288 UTF-8 bytes", ); - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript({ runId: RUN_ID }); const sourceEventId = state.translateUpdate({ update: { content: { text: "hello", type: "text" }, @@ -723,12 +676,7 @@ describe("ACP runtime event translation", () => { }); test("normalizes ACP usage sources to the mosoo usage contract", () => { - const state = new AcpAssistantTranscriptState(); - - state.begin({ - messageId: "message-1", - runId: RUN_ID, - }); + const state = beginAcpTranscript({ runId: RUN_ID }); const sessionUsage = state.translateUpdate({ update: { @@ -785,8 +733,7 @@ describe("ACP runtime event translation", () => { ["unsafe", Number.MAX_SAFE_INTEGER + 1], ["non-finite", Number.POSITIVE_INFINITY], ] as const)("drops wholly invalid %s usage without blocking Run completion", (_name, value) => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript({ runId: RUN_ID }); state.translateUpdate({ update: { content: { text: "done", type: "text" }, @@ -816,8 +763,7 @@ describe("ACP runtime event translation", () => { }); test("keeps valid usage fields while omitting malformed siblings", () => { - const state = new AcpAssistantTranscriptState(); - state.begin({ messageId: "message-1", runId: RUN_ID }); + const state = beginAcpTranscript({ runId: RUN_ID }); const [sessionUsage] = state.translateUpdate({ update: { diff --git a/tests/acp-provider-fixtures.test.ts b/tests/acp-provider-fixtures.test.ts index d45b850..07a57fa 100644 --- a/tests/acp-provider-fixtures.test.ts +++ b/tests/acp-provider-fixtures.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test"; import type { StopReason } from "@agentclientprotocol/sdk"; -import { readFileSync } from "node:fs"; import type { DriverEventInput } from "../src/protocol/events"; import { @@ -8,36 +7,29 @@ import { type AcpAssistantTranscriptStateInput, } from "../src/runtimes/acp/acp-assistant-transcript-state"; import { toSessionReadyEvents } from "../src/runtimes/acp/acp-session-events"; - -interface CompletePromptFixture { - readonly stopReason: StopReason; - readonly usage: unknown; -} - -interface FailPromptFixture { - readonly code: string; - readonly message: string; - readonly recoverable?: boolean | undefined; -} - -interface PermissionRequestFixture { - readonly params: unknown; - readonly requestId: string; -} - -interface SessionReadyFixture { - readonly mode: "created" | "loaded" | "resumed"; - readonly nativeSessionId: string; - readonly setup: Record; -} +import { normalizeAcpProviderEvents, readProviderFixture } from "./provider-fixture-test-helpers"; interface AcpProviderFixtureCase { readonly begin?: AcpAssistantTranscriptStateInput | undefined; - readonly completePrompt?: CompletePromptFixture | undefined; + readonly completePrompt?: + | { readonly stopReason: StopReason; readonly usage: unknown } + | undefined; readonly expectedEvents: readonly unknown[]; - readonly failPrompt?: FailPromptFixture | undefined; - readonly permissionRequest?: PermissionRequestFixture | undefined; - readonly sessionReady?: SessionReadyFixture | undefined; + readonly failPrompt?: + | { + readonly code: string; + readonly message: string; + readonly recoverable?: boolean | undefined; + } + | undefined; + readonly permissionRequest?: { readonly params: unknown; readonly requestId: string } | undefined; + readonly sessionReady?: + | { + readonly mode: "created" | "loaded" | "resumed"; + readonly nativeSessionId: string; + readonly setup: Record; + } + | undefined; readonly updates: readonly unknown[]; } @@ -49,235 +41,6 @@ const acpFixtureNames = [ "turn-text-tool-usage", ] as const; -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function readJsonFixture(path: string): unknown { - return JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")); -} - -function readStringField(record: Record, field: string): string { - const value = record[field]; - - if (typeof value !== "string") { - throw new Error(`ACP provider fixture field ${field} must be a string.`); - } - - return value; -} - -function readRecordField(record: Record, field: string): Record { - const value = record[field]; - - if (!isRecord(value)) { - throw new Error(`ACP provider fixture field ${field} must be an object.`); - } - - return value; -} - -function readBeginFixture(value: unknown): AcpAssistantTranscriptStateInput | undefined { - if (value === undefined) { - return undefined; - } - - if (!isRecord(value)) { - throw new Error("ACP provider fixture begin must be an object."); - } - - return { - messageId: readStringField(value, "messageId"), - runId: readStringField(value, "runId") as AcpAssistantTranscriptStateInput["runId"], - }; -} - -function readCompletePromptFixture(value: unknown): CompletePromptFixture | undefined { - if (value === undefined) { - return undefined; - } - - if (!isRecord(value)) { - throw new Error("ACP provider fixture completePrompt must be an object."); - } - - const stopReason = readStringField(value, "stopReason"); - - if ( - stopReason !== "cancelled" && - stopReason !== "end_turn" && - stopReason !== "max_tokens" && - stopReason !== "max_turn_requests" && - stopReason !== "refusal" - ) { - throw new Error("ACP provider fixture completePrompt stopReason is unsupported."); - } - - return { - stopReason, - usage: value["usage"], - }; -} - -function readFailPromptFixture(value: unknown): FailPromptFixture | undefined { - if (value === undefined) { - return undefined; - } - - if (!isRecord(value)) { - throw new Error("ACP provider fixture failPrompt must be an object."); - } - - const recoverable = value["recoverable"]; - - if (recoverable !== undefined && typeof recoverable !== "boolean") { - throw new Error("ACP provider fixture failPrompt recoverable must be a boolean."); - } - - return { - code: readStringField(value, "code"), - message: readStringField(value, "message"), - ...(recoverable === undefined ? {} : { recoverable }), - }; -} - -function readPermissionRequestFixture(value: unknown): PermissionRequestFixture | undefined { - if (value === undefined) { - return undefined; - } - - if (!isRecord(value)) { - throw new Error("ACP provider fixture permissionRequest must be an object."); - } - - return { - params: value["params"], - requestId: readStringField(value, "requestId"), - }; -} - -function readSessionReadyFixture(value: unknown): SessionReadyFixture | undefined { - if (value === undefined) { - return undefined; - } - - if (!isRecord(value)) { - throw new Error("ACP provider fixture sessionReady must be an object."); - } - - const mode = readStringField(value, "mode"); - - if (mode !== "created" && mode !== "loaded" && mode !== "resumed") { - throw new Error("ACP provider fixture sessionReady mode is unsupported."); - } - - return { - mode, - nativeSessionId: readStringField(value, "nativeSessionId"), - setup: readRecordField(value, "setup"), - }; -} - -function readAcpProviderFixtureCase(path: string): AcpProviderFixtureCase { - const fixture = readJsonFixture(path); - - if (!isRecord(fixture)) { - throw new Error("ACP provider fixture must be an object."); - } - - const updates = fixture["updates"] ?? []; - const expectedEvents = fixture["expectedEvents"]; - - if (!Array.isArray(updates) || !Array.isArray(expectedEvents)) { - throw new Error("ACP provider fixture updates and expectedEvents must be arrays."); - } - - return { - begin: readBeginFixture(fixture["begin"]), - completePrompt: readCompletePromptFixture(fixture["completePrompt"]), - expectedEvents, - failPrompt: readFailPromptFixture(fixture["failPrompt"]), - permissionRequest: readPermissionRequestFixture(fixture["permissionRequest"]), - sessionReady: readSessionReadyFixture(fixture["sessionReady"]), - updates, - }; -} - -function stripUndefined(value: unknown): unknown { - if (Array.isArray(value)) { - return value.map(stripUndefined); - } - - if (!isRecord(value)) { - return value; - } - - const entries = Object.entries(value).flatMap(([key, entry]): [string, unknown][] => - entry === undefined ? [] : [[key, stripUndefined(entry)]], - ); - - return Object.fromEntries(entries); -} - -function readAssistantMessageId(event: DriverEventInput): string | null { - if (event.kind !== "message.started" || !isRecord(event.payload)) { - return null; - } - - return event.payload["role"] === "agent" ? readStringField(event.payload, "messageId") : null; -} - -function replaceAssistantMessageIds(value: unknown, aliases: ReadonlyMap): unknown { - if (typeof value === "string") { - return aliases.get(value) ?? value; - } - - if (Array.isArray(value)) { - return value.map((entry) => replaceAssistantMessageIds(entry, aliases)); - } - - if (!isRecord(value)) { - return value; - } - - return Object.fromEntries( - Object.entries(value).map(([key, entry]) => [key, replaceAssistantMessageIds(entry, aliases)]), - ); -} - -function normalizeAcpEvent( - event: DriverEventInput, - aliases: ReadonlyMap, -): Record { - const eventRecord = stripUndefined(event); - - if (!isRecord(eventRecord)) { - throw new Error("ACP translator event must be an object."); - } - - const normalized = replaceAssistantMessageIds(eventRecord, aliases); - - if (!isRecord(normalized)) { - throw new Error("ACP normalized event must be an object."); - } - - return normalized; -} - -function normalizeAcpEvents(events: readonly DriverEventInput[]): Record[] { - const aliases = new Map(); - - for (const event of events) { - const messageId = readAssistantMessageId(event); - - if (messageId !== null && !aliases.has(messageId)) { - aliases.set(messageId, `assistant-message-${aliases.size + 1}`); - } - } - - return events.map((event) => normalizeAcpEvent(event, aliases)); -} - function appAcpFixture(fixture: AcpProviderFixtureCase): DriverEventInput[] { const state = new AcpAssistantTranscriptState(); const events: DriverEventInput[] = []; @@ -321,8 +84,11 @@ function appAcpFixture(fixture: AcpProviderFixtureCase): DriverEventInput[] { describe("ACP provider fixtures", () => { test.each(acpFixtureNames)("apps provider-native fixture %s", (name) => { - const fixture = readAcpProviderFixtureCase(`./fixtures/providers/acp/cases/${name}.json`); + const fixture = readProviderFixture( + `./fixtures/providers/acp/cases/${name}.json`, + { arrays: ["expectedEvents", "updates"] }, + ); - expect(normalizeAcpEvents(appAcpFixture(fixture))).toEqual(fixture.expectedEvents); + expect(normalizeAcpProviderEvents(appAcpFixture(fixture))).toEqual(fixture.expectedEvents); }); }); diff --git a/tests/acp-session-setup.test.ts b/tests/acp-session-setup.test.ts index 20fff57..41ce147 100644 --- a/tests/acp-session-setup.test.ts +++ b/tests/acp-session-setup.test.ts @@ -3,7 +3,7 @@ import type { AgentCapabilities, ClientContext } from "@agentclientprotocol/sdk" import { describe, expect, test } from "bun:test"; import { setupAcpSession } from "../src/runtimes/acp/acp-session-setup"; -import { driverBootPayload, driverStartInput } from "./driver-boot-payload-fixture"; +import { driverStartInput } from "./driver-boot-payload-fixture"; const EXISTING_SESSION_ID = "native-session-existing"; @@ -49,7 +49,6 @@ function setupInput(input: { return { ...input, payload: input.payload ?? driverStartInput, - sessionContext: driverBootPayload.execution.session.context, }; } @@ -91,6 +90,10 @@ describe("ACP session setup", () => { ); expect(requests[0]?.params["additionalDirectories"]).toEqual(["/workspace/extra"]); + expect(requests[0]?.params["_meta"]).toEqual({ + "mosoo.ai/origin": driverStartInput.execution.session.context.origin, + "mosoo.ai/sessionContext": driverStartInput.execution.session.context, + }); }); test("prefers resume without entering the load replay scope", async () => { diff --git a/tests/acp-terminal-manager.test.ts b/tests/acp-terminal-manager.test.ts index 8725605..af0b98b 100644 --- a/tests/acp-terminal-manager.test.ts +++ b/tests/acp-terminal-manager.test.ts @@ -54,6 +54,26 @@ function createHarness( return { context, events, manager }; } +async function waitForTerminalOutput( + manager: AcpTerminalManager, + terminal: unknown, + expected: "non-empty" | "ready" = "ready", +): Promise { + const deadline = Date.now() + 3_000; + + for (;;) { + const output = manager.output(terminal).output; + + if (expected === "non-empty" ? output.length > 0 : output === expected) { + return; + } + if (Date.now() >= deadline) { + throw new Error(`Timed out waiting for terminal output ${expected}.`); + } + await Bun.sleep(10); + } +} + function isProcessRunning(pid: number): boolean { try { process.kill(pid, 0); @@ -167,9 +187,7 @@ describe("ACP terminal manager", () => { args: ["-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)'], command: process.execPath, }); - while (harness.manager.output(terminal).output !== "ready") { - await Bun.sleep(10); - } + await waitForTerminalOutput(harness.manager, terminal); const killed = harness.manager.kill(harness.context, terminal); let settled = false; void killed.then(() => { @@ -203,9 +221,7 @@ describe("ACP terminal manager", () => { args: ["-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)'], command: process.execPath, }); - while (harness.manager.output(terminal).output !== "ready") { - await Bun.sleep(10); - } + await waitForTerminalOutput(harness.manager, terminal); const released = harness.manager.release(harness.context, terminal); const pending = await settlePromiseWithTimeout(released, { label: "terminal release cleanup barrier", @@ -243,9 +259,7 @@ describe("ACP terminal manager", () => { args: ["-e", "process.stdout.write(String(process.pid));setInterval(() => {}, 1000)"], command: process.execPath, }); - while (harness.manager.output(terminal).output === "") { - await Bun.sleep(10); - } + await waitForTerminalOutput(harness.manager, terminal, "non-empty"); const exited = harness.manager.waitForExit(terminal); void exited.catch(() => {}); process.kill(Number.parseInt(harness.manager.output(terminal).output, 10), "SIGTERM"); @@ -292,9 +306,7 @@ describe("ACP terminal manager", () => { ], command: process.execPath, }); - while (harness.manager.output(terminal).output !== "ready") { - await Bun.sleep(10); - } + await waitForTerminalOutput(harness.manager, terminal); pid = await waitForPidFile(pidPath); if (outcome === "completed") { @@ -328,9 +340,7 @@ describe("ACP terminal manager", () => { args: ["-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)'], command: process.execPath, }); - while (harness.manager.output(terminal).output !== "ready") { - await Bun.sleep(10); - } + await waitForTerminalOutput(harness.manager, terminal); cleanup.reject(new Error("test watchdog cleanup failed")); await expect(harness.manager.waitForExit(terminal)).rejects.toThrow("watchdog failed"); @@ -566,9 +576,7 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 args: ["-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)'], command: process.execPath, }); - while (harness.manager.output(first).output !== "ready") { - await Bun.sleep(10); - } + await waitForTerminalOutput(harness.manager, first); await harness.manager.stopTurn(harness.context, firstTurn); expect(() => harness.manager.output(first)).toThrow("does not exist"); diff --git a/tests/acp-test-helpers.ts b/tests/acp-test-helpers.ts new file mode 100644 index 0000000..1ef0acb --- /dev/null +++ b/tests/acp-test-helpers.ts @@ -0,0 +1,27 @@ +import type { RunId } from "../src/protocol/id"; +import { + AcpAssistantTranscriptState, + type AcpAssistantTranscriptStateInput, +} from "../src/runtimes/acp/acp-assistant-transcript-state"; + +export function beginAcpTranscript( + input: Partial = {}, +): AcpAssistantTranscriptState { + const state = new AcpAssistantTranscriptState(); + state.begin({ messageId: "message-1", runId: "run-1" as RunId, ...input }); + return state; +} + +export async function waitForAcpTestCondition( + condition: () => boolean | Promise, + description: string, +): Promise { + for (let attempt = 0; attempt < 100; attempt += 1) { + if (await condition()) { + return; + } + await Bun.sleep(5); + } + + throw new Error(`Timed out waiting for ${description}.`); +} diff --git a/tests/agent-driver-kernel-async-queue.test.ts b/tests/agent-driver-kernel-async-queue.test.ts index 4349344..ce0c0d2 100644 --- a/tests/agent-driver-kernel-async-queue.test.ts +++ b/tests/agent-driver-kernel-async-queue.test.ts @@ -419,7 +419,9 @@ describe("AgentDriverKernelCore", () => { expect(startInput).not.toHaveProperty("traceparent"); expect(startInput.execution).not.toHaveProperty("configRevision"); expect(startInput.execution).toHaveProperty("run"); - expect(startInput.execution.session).not.toHaveProperty("context"); + expect(startInput.execution.session.context).toEqual( + driverBootPayload.execution.session.context, + ); expect(startInput.execution.session).toHaveProperty("sharedRootPath"); }); diff --git a/tests/architecture-boundaries.test.ts b/tests/architecture-boundaries.test.ts index b142fa8..2818eb1 100644 --- a/tests/architecture-boundaries.test.ts +++ b/tests/architecture-boundaries.test.ts @@ -2,384 +2,78 @@ import { describe, expect, test } from "bun:test"; import { dirname, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; -import { API, type Project } from "typescript/unstable/async"; -import { - SyntaxKind, - type CallExpression, - type Node, - type SourceFile, -} from "typescript/unstable/ast"; -import { - isCallExpression, - isElementAccessExpression, - isExportAssignment, - isExportDeclaration, - isFunctionDeclaration, - isIdentifier, - isImportDeclaration, - isImportEqualsDeclaration, - isMetaProperty, - isMethodSignatureDeclaration, - isNamedExports, - isNamedImports, - isPropertyAccessExpression, - isStringLiteralLikeNode, -} from "typescript/unstable/ast/is"; - const repositoryRoot = fileURLToPath(new URL("../", import.meta.url)); -const packageManifestPath = resolve(repositoryRoot, "package.json"); +const packageManifest = resolve(repositoryRoot, "package.json"); const sourceRoot = resolve(repositoryRoot, "src"); +const transpiler = new Bun.Transpiler({ loader: "ts" }); interface SourceGraph { readonly dependencies: ReadonlyMap>; readonly files: readonly string[]; } -interface ArchitectureAudit { - readonly graph: SourceGraph; - readonly resolutionDependencies: ReadonlySet; - readonly resolutionFixtureError: string | undefined; - readonly runtimeLoaderFixtureErrors: ReadonlyMap; - readonly unsupportedFixtureErrors: readonly (string | undefined)[]; -} - -function sourcePath(path: string): string { - return relative(repositoryRoot, path).split(sep).join("/"); -} - -function isImportMetaRequire(node: Node): boolean { - if (!isPropertyAccessExpression(node) && !isElementAccessExpression(node)) { - return false; - } - - if ( - !isMetaProperty(node.expression) || - node.expression.keywordToken !== SyntaxKind.ImportKeyword - ) { - return false; - } - - if (isPropertyAccessExpression(node)) { - return node.name.text === "require"; - } - - return ( - isStringLiteralLikeNode(node.argumentExpression) && node.argumentExpression.text === "require" - ); -} - -function isTypeOnlyModuleDeclaration(node: Node): boolean { - if (isImportDeclaration(node)) { - const clause = node.importClause; - - return ( - clause?.phaseModifier === SyntaxKind.TypeKeyword || - (clause?.name === undefined && - clause?.namedBindings !== undefined && - isNamedImports(clause.namedBindings) && - clause.namedBindings.elements.length > 0 && - clause.namedBindings.elements.every((element) => element.isTypeOnly)) - ); - } - - return ( - isExportDeclaration(node) && - (node.isTypeOnly || - (node.exportClause !== undefined && - isNamedExports(node.exportClause) && - node.exportClause.elements.length > 0 && - node.exportClause.elements.every((element) => element.isTypeOnly))) - ); -} - -async function assertStaticEsm(project: Project, file: string, root: Node): Promise { - const commonJsIdentifiers: Node[] = []; - const calls: CallExpression[] = []; - - function visit(node: Node): void { - if (isCallExpression(node)) { - if (node.expression.kind === SyntaxKind.ImportKeyword) { - throw new Error(`Dynamic imports are unsupported: ${sourcePath(file)}.`); - } - - calls.push(node); - } - - if (isImportMetaRequire(node)) { - throw new Error(`Runtime module loaders are unsupported: ${sourcePath(file)}.`); - } - - if ( - (isImportDeclaration(node) || isExportDeclaration(node)) && - !isTypeOnlyModuleDeclaration(node) && - node.moduleSpecifier !== undefined && - isStringLiteralLikeNode(node.moduleSpecifier) && - ["module", "node:module"].includes(node.moduleSpecifier.text) - ) { - throw new Error(`Runtime module loaders are unsupported: ${sourcePath(file)}.`); - } - - if (isImportEqualsDeclaration(node) || (isExportAssignment(node) && node.isExportEquals)) { - throw new Error(`CommonJS syntax is unsupported: ${sourcePath(file)}.`); - } - - if ( - isIdentifier(node) && - ["exports", "module", "require"].includes(node.text) && - !(isPropertyAccessExpression(node.parent) && node.parent.name === node) - ) { - commonJsIdentifiers.push(node); - } - - node.forEachChild(visit); - } - - visit(root); - const symbols = await project.checker.getSymbolAtLocation(commonJsIdentifiers); - const sourceFile = root.getSourceFile().fileName; - - for (const symbol of symbols) { - if ( - symbol === undefined || - (await project.checker.isUnknownSymbol(symbol)) || - !symbol.declarations.some(({ path }) => path === sourceFile) - ) { - throw new Error(`CommonJS syntax is unsupported: ${sourcePath(file)}.`); - } - } - - for (const call of calls) { - const declaration = (await project.checker.getResolvedSignature(call))?.declaration; - - if ( - declaration?.path.endsWith(`${sep}@types${sep}node${sep}process.d.ts`) && - declaration.kind === SyntaxKind.MethodSignature - ) { - const node = await declaration.resolve(project); - - if ( - node !== undefined && - isMethodSignatureDeclaration(node) && - isIdentifier(node.name) && - node.name.text === "getBuiltinModule" - ) { - throw new Error(`Runtime module loaders are unsupported: ${sourcePath(file)}.`); - } - } - } -} - -async function captureError(check: () => Promise): Promise { - try { - await check(); - return undefined; - } catch (error) { - return error instanceof Error ? error.message : String(error); - } -} - -function isWorkspaceSpecifier(specifier: string): boolean { - return ( - specifier.startsWith(".") || - specifier.startsWith("/") || - specifier.startsWith("#") || - specifier.startsWith("file:") - ); -} - -function isDependencyPath(path: string): boolean { - return path.includes(`${sep}node_modules${sep}`); -} - function admitSourceDependency( importer: string, specifier: string, target: string, files: ReadonlySet, ): string | undefined { - if (target === packageManifestPath) { + if (target === packageManifest) { return undefined; } - - if (target.startsWith(`${sourceRoot}${sep}`)) { - if (!files.has(target)) { - throw new Error(`Source import resolved to an unsupported file: ${sourcePath(target)}.`); - } - - return target; + if (!target.startsWith(`${sourceRoot}${sep}`)) { + throw new Error( + `Source import resolves outside the source root: ${sourcePath(importer)} -> ${specifier}.`, + ); } - - if (!isWorkspaceSpecifier(specifier) && isDependencyPath(target)) { + if (target.endsWith(".json")) { return undefined; } + if (!files.has(target)) { + throw new Error(`Source import resolved to an unsupported file: ${sourcePath(target)}.`); + } - throw new Error( - `Source import resolves outside the source root: ${sourcePath(importer)} -> ${specifier}.`, - ); + return target; } -async function resolveModuleDependencies( - project: Project, - importer: string, - source: SourceFile, - files: ReadonlySet, -): Promise> { - const dependencies = new Set(); - const moduleSpecifiers = [ - ...source.imports, - ...source.moduleAugmentations.filter((node) => !(isIdentifier(node) && node.text === "global")), - ]; - const symbols = await project.checker.getSymbolAtLocation(moduleSpecifiers); - - for (const [index, node] of moduleSpecifiers.entries()) { - if (!isStringLiteralLikeNode(node)) { - throw new Error(`Source import is not statically auditable: ${sourcePath(importer)}.`); - } - - const symbol = symbols[index]; - - if (symbol === undefined || (await project.checker.isUnknownSymbol(symbol))) { - throw new Error(`Source import cannot be resolved: ${sourcePath(importer)} -> ${node.text}.`); - } - - const declarations = symbol.declarations.filter(({ kind }) => kind === SyntaxKind.SourceFile); +async function createSourceGraph(): Promise { + const files = ( + await Array.fromAsync( + new Bun.Glob("src/**/*.ts").scan({ absolute: true, cwd: repositoryRoot, onlyFiles: true }), + ) + ).toSorted(); + const fileSet = new Set(files); + const dependencies = new Map>(); - if (declarations.length === 0) { - if ( - !isWorkspaceSpecifier(node.text) && - symbol.declarations.length > 0 && - symbol.declarations.every(({ path }) => isDependencyPath(path)) - ) { - continue; - } + await Promise.all( + files.map(async (file) => { + const source = (await Bun.file(file).text()).replace(/^#![^\n]*\n/, ""); + const resolved = new Set(); - throw new Error( - `Source import has no file declaration: ${sourcePath(importer)} -> ${node.text}.`, - ); - } - - for (const declaration of declarations) { - const dependency = admitSourceDependency(importer, node.text, declaration.path, files); + for (const dependency of transpiler.scanImports(source)) { + if (dependency.kind !== "import-statement") { + throw new Error(`Runtime module loaders are unsupported: ${sourcePath(file)}.`); + } + if (!dependency.path.startsWith(".")) { + continue; + } - if (dependency !== undefined) { - dependencies.add(dependency); + const target = Bun.resolveSync(dependency.path, dirname(file)); + const sourceDependency = admitSourceDependency(file, dependency.path, target, fileSet); + if (sourceDependency !== undefined) { + resolved.add(sourceDependency); + } } - } - } - - for (const reference of source.referencedFiles) { - const target = await project.program.getSourceFile( - resolve(dirname(importer), reference.fileName), - ); - - if (target === undefined) { - throw new Error( - `Source reference cannot be resolved: ${sourcePath(importer)} -> ${reference.fileName}.`, - ); - } - - const dependency = admitSourceDependency(importer, reference.fileName, target.fileName, files); - - if (dependency !== undefined) { - dependencies.add(dependency); - } - } - - return dependencies; -} -async function getFixture(project: Project, name: string): Promise { - const fixture = await project.program.getSourceFile( - resolve(repositoryRoot, `tests/fixtures/${name}.ts`), + dependencies.set(file, resolved); + }), ); - if (fixture === undefined) { - throw new Error(`TypeScript did not load the ${name} fixture.`); - } - - return fixture; + return { dependencies, files }; } -async function createArchitectureAudit(): Promise { - const api = new API({ cwd: repositoryRoot }); - const configPath = resolve(repositoryRoot, "tsconfig.json"); - - try { - const snapshot = await api.updateSnapshot({ openProjects: [configPath] }); - const project = snapshot.getProject(configPath); - - if (project === undefined) { - throw new Error(`TypeScript project is missing: ${configPath}.`); - } - - const files = (await project.program.getSourceFileNames()) - .filter( - (path) => path.startsWith(`${sourceRoot}${sep}`) && !/\.d\.(?:cts|mts|ts)$/.test(path), - ) - .toSorted(); - const fileSet = new Set(files); - const dependencies = new Map>(); - - await Promise.all( - files.map(async (file) => { - const source = await project.program.getSourceFile(file); - - if (source === undefined) { - throw new Error(`TypeScript did not load ${sourcePath(file)}.`); - } - - await assertStaticEsm(project, file, source); - dependencies.set(file, await resolveModuleDependencies(project, file, source, fileSet)); - }), - ); - - const [auditFixture, resolutionFixture, runtimeLoaderFixture] = await Promise.all([ - getFixture(project, "architecture-boundaries-audit"), - getFixture(project, "architecture-boundaries-resolution"), - getFixture(project, "architecture-boundaries-runtime-loaders"), - ]); - const resolutionDependencies = await resolveModuleDependencies( - project, - resolutionFixture.fileName, - resolutionFixture, - fileSet, - ); - const unsupportedFixtureErrors = await Promise.all( - auditFixture.statements - .slice(1) - .map((statement) => - captureError(() => assertStaticEsm(project, auditFixture.fileName, statement)), - ), - ); - const resolutionFixtureError = await captureError(() => - assertStaticEsm(project, resolutionFixture.fileName, resolutionFixture), - ); - const runtimeLoaderFixtureErrors = new Map( - await Promise.all( - runtimeLoaderFixture.statements - .filter(isFunctionDeclaration) - .map( - async (statement) => - [ - statement.name!.text, - await captureError(() => - assertStaticEsm(project, runtimeLoaderFixture.fileName, statement), - ), - ] as const, - ), - ), - ); - - return { - graph: { dependencies, files }, - resolutionDependencies, - resolutionFixtureError, - runtimeLoaderFixtureErrors, - unsupportedFixtureErrors, - }; - } finally { - await api.close(); - } +function sourcePath(path: string): string { + return relative(repositoryRoot, path).split(sep).join("/"); } function isWithin(path: string, directory: string): boolean { @@ -393,6 +87,21 @@ function isProviderRuntime(path: string): boolean { ); } +function reachableDependencies(graph: SourceGraph, file: string): ReadonlySet { + const reachable = new Set(); + const pending = [...(graph.dependencies.get(file) ?? [])]; + + for (const dependency of pending) { + if (reachable.has(dependency)) { + continue; + } + reachable.add(dependency); + pending.push(...(graph.dependencies.get(dependency) ?? [])); + } + + return reachable; +} + function boundaryViolations(graph: SourceGraph): string[] { const violations: string[] = []; @@ -400,11 +109,11 @@ function boundaryViolations(graph: SourceGraph): string[] { for (const dependency of reachableDependencies(graph, file)) { const forbidden = (isWithin(file, "contract") && - ["core", "runtimes", "infrastructure", "stores", "surfaces"].some((directory) => + ["core", "infrastructure", "runtimes", "stores", "surfaces"].some((directory) => isWithin(dependency, directory), )) || (isWithin(file, "protocol") && - ["core", "runtimes", "infrastructure", "runtime-events"].some((directory) => + ["core", "infrastructure", "runtimes", "runtime-events"].some((directory) => isWithin(dependency, directory), )) || (isWithin(file, "core") && @@ -426,24 +135,6 @@ function boundaryViolations(graph: SourceGraph): string[] { return violations.toSorted(); } -function reachableDependencies(graph: SourceGraph, file: string): ReadonlySet { - const reachable = new Set(); - const pending = [...(graph.dependencies.get(file) ?? [])]; - - while (pending.length > 0) { - const dependency = pending.pop()!; - - if (reachable.has(dependency)) { - continue; - } - - reachable.add(dependency); - pending.push(...(graph.dependencies.get(dependency) ?? [])); - } - - return reachable; -} - function compositionViolations(graph: SourceGraph): string[] { return graph.files .filter((file) => !isWithin(file, "bin")) @@ -469,87 +160,67 @@ function bidirectionalDirectories(graph: SourceGraph): string[] { for (const file of graph.files) { const source = sourceDirectory(file); - for (const dependency of graph.dependencies.get(file) ?? []) { const target = sourceDirectory(dependency); - if (source !== target) { edges.add(`${source}\0${target}`); } } } - const pairs = new Set(); - - for (const edge of edges) { - const [source, target] = edge.split("\0") as [string, string]; - - if (edges.has(`${target}\0${source}`)) { - pairs.add([source, target].toSorted().join(" <-> ")); - } - } - - return [...pairs].toSorted(); + return [...edges] + .filter((edge) => { + const [source, target] = edge.split("\0") as [string, string]; + return edges.has(`${target}\0${source}`); + }) + .map((edge) => edge.split("\0").toSorted().join(" <-> ")) + .filter((pair, index, pairs) => pairs.indexOf(pair) === index) + .toSorted(); } -const audit = await createArchitectureAudit(); -const graph = audit.graph; +const graph = await createSourceGraph(); describe("source architecture", () => { - test("uses TypeScript resolution for every dependency syntax", () => { - expect(audit.resolutionDependencies).toEqual( - new Set([ - resolve(sourceRoot, "contract/index.ts"), - resolve(sourceRoot, "core/agent-driver-kernel.ts"), - resolve(sourceRoot, "core/driver-runtime-state.ts"), - resolve(sourceRoot, "paths.ts"), - ]), - ); - }); - - test("allows only the exact root package manifest outside the source root", () => { + test("allows only source JSON leaves and the root package manifest outside the TS graph", () => { const importer = resolve(sourceRoot, "runtime.ts"); - const files = new Set(graph.files); + const files = new Set([importer]); - expect(admitSourceDependency(importer, "../package.json", packageManifestPath, files)).toBe( - undefined, - ); + expect( + admitSourceDependency(importer, "../package.json", packageManifest, files), + ).toBeUndefined(); + expect( + admitSourceDependency(importer, "./schema.json", resolve(sourceRoot, "schema.json"), files), + ).toBeUndefined(); + expect(() => + admitSourceDependency(importer, "./README.md", resolve(sourceRoot, "README.md"), files), + ).toThrow("unsupported file"); expect(() => - admitSourceDependency(importer, "../README.md", resolve(repositoryRoot, "README.md"), files), + admitSourceDependency( + importer, + "../schema.json", + resolve(repositoryRoot, "schema.json"), + files, + ), ).toThrow("outside the source root"); }); - test("rejects runtime-only module loading without rejecting static ESM or local names", () => { - for (const error of audit.unsupportedFixtureErrors) { - expect(error).toMatch( - /(?:CommonJS syntax is|Dynamic imports are|Runtime module loaders are) unsupported/, - ); - } - expect(audit.resolutionFixtureError).toBeUndefined(); - for (const name of ["computed", "destructured", "direct", "imported"]) { - expect(audit.runtimeLoaderFixtureErrors.get(name)).toContain( - "Runtime module loaders are unsupported", - ); - } - expect(audit.runtimeLoaderFixtureErrors.get("local")).toBeUndefined(); - }); - - test("keeps contract, protocol, core, and provider imports pointing inward", () => { + test("keeps transitive contract, protocol, core, and provider dependencies inward", () => { expect(boundaryViolations(graph)).toEqual([]); const core = resolve(sourceRoot, "core/audit.ts"); const wrapper = resolve(sourceRoot, "audit/wrapper.ts"); const provider = resolve(sourceRoot, "runtimes/openai/audit.ts"); - const files = [core, wrapper, provider]; - const dependencies = new Map>([ - [core, new Set([wrapper])], - [wrapper, new Set([provider])], - [provider, new Set()], - ]); - - expect(boundaryViolations({ dependencies, files })).toEqual([ - "src/core/audit.ts -> src/runtimes/openai/audit.ts", - ]); + + expect( + boundaryViolations({ + dependencies: new Map([ + [core, new Set([wrapper])], + [wrapper, new Set([provider])], + [provider, new Set()], + ]), + files: [core, wrapper, provider], + }), + ).toEqual(["src/core/audit.ts -> src/runtimes/openai/audit.ts"]); }); test("keeps bin as the only transitive full runtime composition root", () => { @@ -560,25 +231,19 @@ describe("source architecture", () => { const core = resolve(sourceRoot, "core/audit.ts"); const provider = resolve(sourceRoot, "runtimes/openai/audit.ts"); const infrastructure = resolve(sourceRoot, "infrastructure/audit.ts"); - const files = [root, wrapper, core, provider, infrastructure]; - const dependencies = new Map>([ - [root, new Set([wrapper])], - [wrapper, new Set([core, provider, infrastructure])], - [core, new Set()], - [provider, new Set()], - [infrastructure, new Set()], - ]); - - expect(compositionViolations({ dependencies, files })).toEqual([ - "src/audit/wrapper.ts", - "src/composition-audit.ts", - ]); - }); - test("has no file-level dependency cycles", () => { expect( - graph.files.filter((file) => reachableDependencies(graph, file).has(file)).map(sourcePath), - ).toEqual([]); + compositionViolations({ + dependencies: new Map([ + [root, new Set([wrapper])], + [wrapper, new Set([core, provider, infrastructure])], + [core, new Set()], + [provider, new Set()], + [infrastructure, new Set()], + ]), + files: [root, wrapper, core, provider, infrastructure], + }), + ).toEqual(["src/audit/wrapper.ts", "src/composition-audit.ts"]); }); test("has no bidirectional top-level source directory dependencies", () => { diff --git a/tests/claude-agent-sdk-durability.test.ts b/tests/claude-agent-sdk-durability.test.ts index 56e874b..025be2a 100644 --- a/tests/claude-agent-sdk-durability.test.ts +++ b/tests/claude-agent-sdk-durability.test.ts @@ -67,6 +67,7 @@ function createCmaHarness() { const [envelope] = toRuntimeEventInput( { createId: () => createDriverId() as EventId, + driverInstanceId: DRIVER_TEST_IDS.driverInstanceId, occurredAt: "2026-08-13T00:00:00.000Z", runId, sessionId, @@ -456,11 +457,10 @@ describe("Claude Agent SDK durable event boundaries", () => { await translator.failTurn(context, runId, "claude.task_delivery_failed", "delivery failed"); expect( - events.filter((event) => event.kind === "agent.task.updated").map((event) => payload(event)), - ).toMatchObject([ - { active: true, taskId: "task-1" }, - { active: false, taskId: "task-1" }, - ]); + events + .filter((event) => event.kind === "agent.tasks.replaced") + .map((event) => payload(event)), + ).toMatchObject([{ tasks: [{ taskId: "task-1" }] }, { tasks: [] }]); expect(events.at(-1)?.kind).toBe("run.failed"); }); diff --git a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts index 4e98f46..595863d 100644 --- a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts @@ -89,21 +89,26 @@ describe("Claude Agent SDK provider fixtures", () => { await translator.handleSdkMessage(context, message, "run-1" as RunId); } - const taskEvents = events().filter((event) => event.kind === "agent.task.updated"); + const taskEvents = events().filter((event) => event.kind === "agent.tasks.replaced"); expect(taskEvents).toHaveLength(2); expect(taskEvents.at(0)).toMatchObject({ delivery: "lossless", payload: { - active: true, - taskId: "task-1", - taskType: "local_agent", - title: "Inspect the repository", + tasks: [ + { + taskId: "task-1", + taskType: "local_agent", + title: "Inspect the repository", + }, + ], }, + visibility: "participant", }); expect(taskEvents.at(1)).toEqual({ delivery: "lossless", - kind: "agent.task.updated", - payload: { active: false, taskId: "task-1" }, + kind: "agent.tasks.replaced", + payload: { tasks: [] }, + visibility: "participant", }); expect(JSON.stringify(taskEvents)).not.toContain("private"); expect(events().filter((event) => event.kind === "diagnostic.reported")).toHaveLength(4); @@ -146,8 +151,9 @@ describe("Claude Agent SDK provider fixtures", () => { expect(terminalIndex).toBeGreaterThan(0); expect(events().slice(0, terminalIndex)).toContainEqual({ delivery: "lossless", - kind: "agent.task.updated", - payload: { active: false, taskId: "task-1" }, + kind: "agent.tasks.replaced", + payload: { tasks: [] }, + visibility: "participant", }); const beforeReset = events().length; @@ -171,13 +177,17 @@ describe("Claude Agent SDK provider fixtures", () => { ); expect(events().slice(beforeReset)).toContainEqual({ delivery: "lossless", - kind: "agent.task.updated", + kind: "agent.tasks.replaced", payload: { - active: true, - taskId: "task-1", - taskType: "local_agent", - title: "Inspect the repository again", + tasks: [ + { + taskId: "task-1", + taskType: "local_agent", + title: "Inspect the repository again", + }, + ], }, + visibility: "participant", }); }); diff --git a/tests/claude-agent-sdk-task-events.test.ts b/tests/claude-agent-sdk-task-events.test.ts index a43d126..b4f4d00 100644 --- a/tests/claude-agent-sdk-task-events.test.ts +++ b/tests/claude-agent-sdk-task-events.test.ts @@ -2,8 +2,10 @@ import { describe, expect, test } from "bun:test"; import type { SDKBackgroundTasksChangedMessage } from "@anthropic-ai/claude-agent-sdk"; -import type { DriverEventInput } from "../src/protocol/events"; -import { ClaudeAgentSdkTaskEvents } from "../src/runtimes/claude/agent-sdk-task-events"; +import { + claudeBackgroundTasksClosedEvent, + projectClaudeBackgroundTasksSnapshot, +} from "../src/runtimes/claude/agent-sdk-task-events"; function backgroundTasks( tasks: SDKBackgroundTasksChangedMessage["tasks"], @@ -30,160 +32,125 @@ function task( }; } -function commit( - projection: ClaudeAgentSdkTaskEvents, - message: SDKBackgroundTasksChangedMessage, -): readonly DriverEventInput[] { - const prepared = projection.prepare(message); - prepared.commit(); - return prepared.events; +function projection(tasks: SDKBackgroundTasksChangedMessage["tasks"]) { + return projectClaudeBackgroundTasksSnapshot(backgroundTasks(tasks)); } -describe("Claude Agent SDK task events", () => { - test("projects replacement snapshots and metadata changes", () => { - const projection = new ClaudeAgentSdkTaskEvents(); +function snapshot(tasks: SDKBackgroundTasksChangedMessage["tasks"]) { + return projection(tasks).snapshot; +} - expect(commit(projection, backgroundTasks([task("task-1")]))).toEqual([ - { - delivery: "lossless", - kind: "agent.task.updated", - payload: { - active: true, - taskId: "task-1", - taskType: "local_agent", - title: "Inspect the repository", - }, - }, - ]); - expect(commit(projection, backgroundTasks([task("task-1")]))).toEqual([]); - expect(commit(projection, backgroundTasks([task("task-1", "Inspect tests")]))).toEqual([ - { - delivery: "lossless", - kind: "agent.task.updated", - payload: { taskId: "task-1", title: "Inspect tests" }, +describe("Claude Agent SDK task snapshots", () => { + test("projects every SDK snapshot as one complete replacement", () => { + const initial = { + delivery: "lossless", + kind: "agent.tasks.replaced", + payload: { + tasks: [ + { + taskId: "task-1", + taskType: "local_agent", + title: "Inspect the repository", + }, + ], }, - ]); + visibility: "participant", + } as const; + + expect(snapshot([task("task-1")])).toEqual(initial); + expect(snapshot([task("task-1")])).toEqual(initial); + expect(snapshot([task("task-1", "Inspect tests")])).toEqual({ + ...initial, + payload: { tasks: [{ ...initial.payload.tasks[0], title: "Inspect tests" }] }, + }); }); - test("treats removal and ambient changes as visibility, not guessed completion", () => { - const projection = new ClaudeAgentSdkTaskEvents(); - commit(projection, backgroundTasks([task("task-1")])); + test("filters ambient tasks and publishes membership removal as an empty snapshot", () => { + const event = snapshot([task("task-1"), task("ambient-task", "private ambient task", true)]); - expect(commit(projection, backgroundTasks([task("task-1", undefined, true)]))).toEqual([ - { - delivery: "lossless", - kind: "agent.task.updated", - payload: { active: false, taskId: "task-1" }, - }, - ]); - expect(commit(projection, backgroundTasks([task("task-1")]))[0]).toMatchObject({ - payload: { active: true, taskId: "task-1" }, + expect(event).toMatchObject({ + kind: "agent.tasks.replaced", + payload: { tasks: [{ taskId: "task-1" }] }, }); - expect(commit(projection, backgroundTasks([]))).toEqual([ - { - delivery: "lossless", - kind: "agent.task.updated", - payload: { active: false, taskId: "task-1" }, - }, - ]); + expect(JSON.stringify(event)).not.toContain("private"); + expect(snapshot([task("task-1", "private", true)])).toEqual(claudeBackgroundTasksClosedEvent()); }); - test("bounds visible tasks, IDs, and text", () => { - const projection = new ClaudeAgentSdkTaskEvents(); - const events = commit( - projection, - backgroundTasks( - Array.from({ length: 256 }, (_, index) => - task(index === 0 ? "界".repeat(257) : `task-${index}`, `${"x".repeat(4_095)}😀tail`), - ), - ), - ); - - expect(events).toHaveLength(256); - expect(events[0]?.payload).toMatchObject({ - taskId: expect.stringMatching(/^claude-task:[a-f0-9]{64}$/), - title: expect.stringMatching(/^x{4095}$/), + test("bounds task IDs, text, counts, and aggregate event size", () => { + const bounded = snapshot([ + task("界".repeat(257), `${"x".repeat(4_095)}😀tail`), + { description: "", task_id: "empty-metadata", task_type: "" }, + ]); + expect(bounded).toMatchObject({ + payload: { + tasks: [ + { + taskId: expect.stringMatching(/^claude-task:[a-f0-9]{64}$/), + title: expect.stringMatching(/^x{4095}$/), + }, + { taskId: "empty-metadata" }, + ], + }, }); - expect(projection.prepareClosure().events).toHaveLength(256); - }); - test("filters ambient prefixes and omits empty metadata", () => { - const projection = new ClaudeAgentSdkTaskEvents(); - const ambient = Array.from({ length: 1_023 }, (_, index) => - task(`ambient-${index}`, "private", true), - ); + const maximum = snapshot(Array.from({ length: 256 }, (_, index) => task(`task-${index}`))); + expect(maximum).toMatchObject({ kind: "agent.tasks.replaced" }); + if (maximum === null) { + throw new Error("Expected a task replacement snapshot."); + } + expect((maximum.payload as { tasks: unknown[] }).tasks).toHaveLength(256); expect( - commit( - projection, - backgroundTasks([...ambient, { description: "", task_id: "visible", task_type: "" }]), - ), - ).toEqual([ - { - delivery: "lossless", - kind: "agent.task.updated", - payload: { active: true, taskId: "visible" }, - }, - ]); - - const oversized = projection.prepare( - backgroundTasks(Array.from({ length: 257 }, (_, index) => task(`visible-${index}`))), - ); - expect(oversized.events).toMatchObject([ - { + projection(Array.from({ length: 257 }, (_, index) => task(`visible-${index}`))), + ).toMatchObject({ + diagnostic: { kind: "diagnostic.reported", payload: { code: "claude.visible_background_tasks_too_many" }, visibility: "owner_debug", }, - ]); - oversized.commit(); - expect(projection.prepareClosure().events).toHaveLength(1); - + snapshot: null, + }); expect( - projection.prepare( - backgroundTasks( - Array.from({ length: 1_025 }, (_, index) => task(`raw-${index}`, "x", true)), - ), - ).events, - ).toMatchObject([ - { + projection(Array.from({ length: 1_025 }, (_, index) => task(`ambient-${index}`, "x", true))), + ).toMatchObject({ + diagnostic: { kind: "diagnostic.reported", payload: { code: "claude.background_tasks_snapshot_too_large" }, }, - ]); - expect(projection.prepareClosure().events).toHaveLength(1); - }); - - test("retains only the durably accepted prefix", () => { - const projection = new ClaudeAgentSdkTaskEvents(); - const prepared = projection.prepare(backgroundTasks([task("task-1"), task("task-2")])); - - expect(prepared.events).toHaveLength(2); - prepared.beforePublish(0); - prepared.commitAccepted(0); - expect(projection.prepareClosure().events).toEqual([ - { + snapshot: null, + }); + const oversizedMetadata = projection( + Array.from({ length: 100 }, (_, index) => ({ + description: "界".repeat(4_096), + task_id: `large-${index}`, + task_type: "界".repeat(4_096), + })), + ); + expect(oversizedMetadata).toMatchObject({ + diagnostic: { + kind: "diagnostic.reported", + payload: { code: "claude.tasks_snapshot_too_large" }, + }, + snapshot: { delivery: "lossless", - kind: "agent.task.updated", - payload: { active: false, taskId: "task-1" }, + kind: "agent.tasks.replaced", + visibility: "participant", }, - ]); + }); + if (oversizedMetadata.snapshot === null) { + throw new Error("Expected an ID-only membership snapshot."); + } + expect( + (oversizedMetadata.snapshot.payload as { tasks: Array> }).tasks, + ).toEqual(Array.from({ length: 100 }, (_, index) => ({ taskId: `large-${index}` }))); }); - test("commits closure only after durable publication", () => { - const projection = new ClaudeAgentSdkTaskEvents(); - commit(projection, backgroundTasks([task("task-1")])); - - const closure = projection.prepareClosure(); - expect(closure.events).toEqual([ - { - delivery: "lossless", - kind: "agent.task.updated", - payload: { active: false, taskId: "task-1" }, - }, - ]); - expect(projection.prepareClosure().events).toEqual(closure.events); - closure.commit(); - expect(projection.prepareClosure().events).toEqual([]); + test("uses an authoritative empty replacement for turn and process closure", () => { + expect(claudeBackgroundTasksClosedEvent()).toEqual({ + delivery: "lossless", + kind: "agent.tasks.replaced", + payload: { tasks: [] }, + visibility: "participant", + }); }); }); diff --git a/tests/contract-wire.test.ts b/tests/contract-wire.test.ts index 4da77a8..22a0604 100644 --- a/tests/contract-wire.test.ts +++ b/tests/contract-wire.test.ts @@ -150,7 +150,6 @@ describe("contract protocol IDs", () => { ["canonical", "01J00000000000000000000009", "01J00000000000000000000009"], ["lowercase", "01j00000000000000000000009", "01J00000000000000000000009"], ["maximum timestamp", "7ZZZZZZZZZZZZZZZZZZZZZZZZZ", "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"], - ["overflowing timestamp", "80000000000000000000000000", "80000000000000000000000000"], ] as const)("accepts and canonicalizes a %s ULID", (_case, input, expected) => { expect(protocolIdSchema.parse(input)).toBe(expected); expect(isDriverId(expected)).toBe(true); @@ -159,6 +158,7 @@ describe("contract protocol IDs", () => { test.each([ ["UUID", "00000000-0000-4000-8000-000000000001"], ["excluded alphabet character", "01J0000000000000000000000I"], + ["overflowing timestamp", "80000000000000000000000000"], ["wrong length", "01J0000000000000000000000"], ] as const)("rejects a %s", (_case, input) => { expect(protocolIdSchema.safeParse(input).success).toBe(false); diff --git a/tests/driver-artifact-contract.test.ts b/tests/driver-artifact-contract.test.ts index 61c7c88..af82109 100644 --- a/tests/driver-artifact-contract.test.ts +++ b/tests/driver-artifact-contract.test.ts @@ -87,6 +87,15 @@ function readWorkflow(path: string): Workflow { return Bun.YAML.parse(readText(path)) as Workflow; } +function readContainerArguments(): Record { + return Object.fromEntries( + [...readText("../Containerfile").matchAll(/^ARG ([A-Z_]+)=(.+)$/gmu)].map(([, name, value]) => [ + name, + value, + ]), + ); +} + function workflowJob(workflow: Workflow, id: string): WorkflowJob { const job = workflow.jobs?.[id]; if (job === undefined) { @@ -129,7 +138,6 @@ describe("driver artifact contract", () => { }); expect(packageJson.types).toBe("./dist/types/index.d.ts"); expect(packageJson.files).toEqual(["dist", "src", "!src/runtimes/openai/generated", "assets"]); - expect(packageJson.files).not.toContain("tests/fixtures"); }); test("keeps public package entries separate from process internals", () => { @@ -156,7 +164,6 @@ describe("driver artifact contract", () => { "src/contract/index.ts", ].toSorted(); - expect(packageJson.scripts?.["build:types"]).toBe("vp pack"); expect(viteConfig.pack).toEqual({ dts: { emitDtsOnly: true }, entry: ["src/*.ts", "src/contract/index.ts"], @@ -189,82 +196,55 @@ describe("driver artifact contract", () => { } }); - test("keeps the root library entry free of boot and transport internals", () => { - const publicApi = readText("../src/index.ts"); - - expect(publicApi).toContain("./core/agent-driver-kernel"); - expect(publicApi).toContain("./runtimes/provider-registry"); - expect(publicApi).toContain("./protocol/runtime"); - expect(publicApi).not.toContain("./bin/driver-process"); - expect(publicApi).not.toContain("./protocol/boot"); - expect(publicApi).not.toContain("./protocol/orpc"); - expect(publicApi).not.toContain("./protocol/paths"); - expect(publicApi).not.toContain("DriverProcess"); - expect(publicApi).not.toContain("DriverBootPayload"); - expect(publicApi).not.toContain("DriverRuntimeClient"); - expect(publicApi).not.toContain("createDriverStartInputFromBootPayload"); - }); - - test("builds and packages only the process runner artifact", () => { - const packageJson = readDriverPackageJson(); - const buildScript = packageJson.scripts?.["build"] ?? ""; - const imageBuildScript = packageJson.scripts?.["build:image"] ?? ""; - const imageTestScript = packageJson.scripts?.["test:image:environment"] ?? ""; - const containerignore = readText("../.containerignore"); - const containerfile = readText("../Containerfile"); - const processEntry = readText("../src/bin/driver.ts"); - - expect(processEntry.startsWith("#!/usr/bin/env bun\n")).toBe(true); - expect(buildScript).toContain("src/bin/driver.ts"); - expect(buildScript).toContain("dist/driver.mjs"); - expect(buildScript).not.toContain("src/index.ts"); - expect(containerfile).toContain("COPY dist/driver.mjs /usr/local/bin/agent-driver"); - expect(containerfile).toContain("RUN chmod +x /usr/local/bin/agent-driver"); - expect(containerfile).toContain("ENV MOSOO_ACP_FALLBACK_COMMAND=opencode"); - expect(containerignore).toContain("!dist/driver.mjs"); - expect(imageBuildScript).toBe( - "vp run build && buildah build --http-proxy=false --platform linux/amd64 -t agent-driver:local .", - ); - expect(imageTestScript).toBe( - "podman run --pull=never --rm --entrypoint node agent-driver:local /usr/local/libexec/mosoo/environment-package-manager-check.mjs smoke", - ); - expect(packageJson.scripts?.["prepack"]).toBe("vp run build"); - }); - - test("pins the OpenAI runtime, CLI, and app-server schema to one stable version", () => { + test("pins container runtimes to package dependency versions", () => { const packageJson = readDriverPackageJson(); const containerfile = readText("../Containerfile"); - const runtimeVersion = packageJson.devDependencies?.["@openai/codex"]; + const versions = { + BUN_VERSION: packageJson.packageManager?.replace("bun@", ""), + CLAUDE_AGENT_SDK_VERSION: packageJson.dependencies?.["@anthropic-ai/claude-agent-sdk"], + OPENAI_RUNTIME_VERSION: packageJson.devDependencies?.["@openai/codex"], + OPENCODE_VERSION: packageJson.devDependencies?.["opencode-ai"], + }; + + expect(readContainerArguments()).toMatchObject(versions); + for (const version of Object.values(versions)) { + expect(version).toMatch(/^\d+\.\d+\.\d+$/); + } + for (const marker of [ + 'test "$(bun --version)" = "$BUN_VERSION"', + "@anthropic-ai/claude-agent-sdk-linux-x64@${CLAUDE_AGENT_SDK_VERSION}", + "opencode-linux-x64-baseline@${OPENCODE_VERSION}", + ]) + expect(containerfile).toContain(marker); - expect(runtimeVersion).toMatch(/^\d+\.\d+\.\d+$/); - expect(containerfile).toContain(`ARG OPENAI_RUNTIME_VERSION=${runtimeVersion}`); + const openAiVersion = versions.OPENAI_RUNTIME_VERSION; expect(readText("../src/runtimes/openai/generated/README.md")).toContain( - `version \`${runtimeVersion}\``, + `version \`${openAiVersion}\``, ); expect(readText("../src/runtimes/openai/generated-json-schema/README.md")).toContain( - `@openai/codex@${runtimeVersion}`, + `@openai/codex@${openAiVersion}`, ); }); - test("routes every live suite through the packed driver artifact", () => { - const packageJson = readDriverPackageJson(); - const artifactScript = packageJson.scripts?.["test:live:artifact"] ?? ""; + test("builds and tests the packed process artifact", () => { + const scripts = readDriverPackageJson().scripts ?? {}; + const containerfile = readText("../Containerfile"); + + expect(scripts["build"]).toContain("src/bin/driver.ts"); + expect(scripts["build"]).toContain("dist/driver.mjs"); + expect(scripts["prepack"]).toBe("vp run build"); + expect(containerfile).toContain("COPY dist/driver.mjs /usr/local/bin/agent-driver"); + expect(scripts["test:live"]).toBe("vp run build && vp run test:live:artifact"); + expect(scripts["test:live:artifact"]).toContain("tests/driver-artifact-mcp.test.ts"); - expect(artifactScript).toContain("AGENT_DRIVER_LIVE=1"); - expect(artifactScript).toContain("tests/driver-artifact-live.test.ts"); - expect(artifactScript).toContain("tests/driver-artifact-mcp.test.ts"); - expect(packageJson.scripts?.["test:live"]).toBe("vp run build && vp run test:live:artifact"); for (const suite of ["anthropic", "openai", "opencode"] as const) { - const script = packageJson.scripts?.[`test:live:${suite}`] ?? ""; - expect(script).toContain("vp run build"); - expect(script).toContain(`AGENT_DRIVER_LIVE_SUITE=${suite}`); - expect(script).toContain("tests/driver-artifact-live.test.ts"); + expect(scripts[`test:live:${suite}`]).toContain("vp run build"); + expect(scripts[`test:live:${suite}`]).toContain(`AGENT_DRIVER_LIVE_SUITE=${suite}`); + expect(scripts[`test:live:${suite}`]).toContain("tests/driver-artifact-live.test.ts"); } }); - test("pins runtime executables and keeps release publication monotonic", () => { - const packageJson = readDriverPackageJson(); - const containerfile = readText("../Containerfile"); + test("keeps release publication ordered, monotonic, and verifiable", () => { const prWorkflow = readWorkflow("../.github/workflows/pr.yml"); const releaseWorkflowText = readText("../.github/workflows/release.yml"); const releaseWorkflow = readWorkflow("../.github/workflows/release.yml"); @@ -274,29 +254,18 @@ describe("driver artifact contract", () => { const npmJob = workflowJob(releaseWorkflow, "publish-npm"); const latestJob = workflowJob(releaseWorkflow, "publish-latest-image"); const verifyRun = workflowStep(verifyJob, "Verify release tag").run ?? ""; - const buildRun = - buildJob.steps?.flatMap(({ run }) => (run === undefined ? [] : [run])).join("\n") ?? ""; const imageRun = workflowStep(imageJob, "Publish versioned image").run ?? ""; const npmRun = workflowStep(npmJob, "Publish package").run ?? ""; + const buildRun = + buildJob.steps?.flatMap(({ run }) => (run === undefined ? [] : [run])).join("\n") ?? ""; const latestRun = latestJob.steps?.flatMap(({ run }) => (run === undefined ? [] : [run])).join("\n") ?? ""; - const claudeVersion = packageJson.dependencies?.["@anthropic-ai/claude-agent-sdk"]; - const openCodeVersion = packageJson.devDependencies?.["opencode-ai"]; - const bunVersion = packageJson.packageManager?.replace("bun@", ""); const actionUses = [prWorkflow, releaseWorkflow].flatMap((workflow) => Object.values(workflow.jobs ?? {}).flatMap((job) => (job.steps ?? []).flatMap((step) => (step.uses === undefined ? [] : [step.uses])), ), ); - expect(containerfile).toContain(`ARG BUN_VERSION=${bunVersion}`); - expect(containerfile).toContain('RUN test "$(bun --version)" = "$BUN_VERSION"'); - expect(containerfile).toContain(`ARG CLAUDE_AGENT_SDK_VERSION=${claudeVersion}`); - expect(containerfile).toContain(`ARG OPENCODE_VERSION=${openCodeVersion}`); - expect(containerfile).toContain( - "@anthropic-ai/claude-agent-sdk-linux-x64@${CLAUDE_AGENT_SDK_VERSION}", - ); - expect(containerfile).toContain("opencode-linux-x64-baseline@${OPENCODE_VERSION}"); expect(releaseWorkflow.concurrency).toEqual({ group: "release", queue: "max" }); expect({ build: buildJob.needs, @@ -360,16 +329,10 @@ describe("driver artifact contract", () => { }); }); - test("keeps the standalone package out of Mosoo workspace dependencies", () => { + test("keeps the standalone package independent of Mosoo workspace packages", () => { const packageJson = readDriverPackageJson(); const deps = Object.keys(packageJson.dependencies ?? {}); - const tsconfig = readText("../tsconfig.json"); expect(deps.filter((dependency) => dependency.startsWith("@mosoo/"))).toEqual([]); - expect(packageJson.dependencies).not.toHaveProperty("@cfworker/json-schema"); - expect(packageJson.dependencies).toHaveProperty("fflate"); - expect(packageJson.dependencies).toHaveProperty("vestig"); - expect(tsconfig).not.toContain("../../dev/"); - expect(tsconfig).not.toContain('"extends"'); }); }); diff --git a/tests/driver-artifact-test-controller.test.ts b/tests/driver-artifact-test-controller.test.ts index 6da1147..d40f8fe 100644 --- a/tests/driver-artifact-test-controller.test.ts +++ b/tests/driver-artifact-test-controller.test.ts @@ -9,6 +9,7 @@ import { type DriverArtifactBootPayload, } from "./driver-artifact-test-controller"; import { + driverRuntimeRpcSchemas, parseDriverCommandUpdateInput, parseDriverFailureInput, parseDriverHelloInput, @@ -255,6 +256,17 @@ describe("driver artifact test controller", () => { }); test("validates structured RPC inputs with the production parsers", () => { + const hello = parseDriverHelloInput({ + capabilities: [{ details: undefined, id: "input_start", status: "supported", version: 1 }], + driverVersion: "test", + ignored: true, + pid: 1, + protocolVersion: 2, + runtime: "acp-fallback", + startedAt: "now", + }); + expect(Object.hasOwn(hello, "ignored")).toBeFalse(); + expect(Object.hasOwn(hello.capabilities[0]!, "details")).toBeFalse(); expect(() => parseDriverHelloInput({ capabilities: [], @@ -286,6 +298,43 @@ describe("driver artifact test controller", () => { status: "completed", }).result, ).toMatchObject({ isError: true }); + expect(() => + parseDriverCommandUpdateInput({ + commandId: "command-1", + driverInstanceId: "driver-1", + result: { outputText: "partial", requestId: "request-1" }, + status: "completed", + }), + ).toThrow("serverId"); + expect(() => + parseDriverCommandUpdateInput({ + commandId: "command-1", + driverInstanceId: "driver-1", + result: { isError: true, requestId: "request-1" }, + status: "completed", + }), + ).toThrow("outputText"); + expect(() => + parseDriverCommandUpdateInput({ + commandId: "command-1", + driverInstanceId: "driver-1", + result: { outputText: undefined, requestId: "request-1" }, + status: "completed", + }), + ).toThrow("outputText"); + expect(() => + parseDriverHelloInput({ + capabilities: [ + { id: "input_start", status: "supported", version: 1 }, + { id: "input_start", status: "unsupported", version: 1 }, + ], + driverVersion: "test", + pid: 1, + protocolVersion: 2, + runtime: "acp-fallback", + startedAt: "now", + }), + ).toThrow("capabilities must not contain duplicate ids"); expect(() => parseDriverFailureInput({ driverInstanceId: "driver-1", @@ -297,6 +346,12 @@ describe("driver artifact test controller", () => { }, }), ).toThrow("driver failure details.nested must be a primitive value"); + const details = JSON.parse('{"__proto__":"preserved"}') as Record; + const failure = parseDriverFailureInput({ + driverInstanceId: "driver-1", + error: { code: "failed", details, message: "failed", retryable: false }, + }); + expect(Object.hasOwn(failure.error.details, "__proto__")).toBeTrue(); expect(() => parseDriverLogBatchInput({ driverInstanceId: "driver-1", @@ -305,6 +360,32 @@ describe("driver artifact test controller", () => { ).toThrow("driver log level is unsupported"); }); + test("reports nested schema failures without throwing from safeParse", () => { + const commandUpdate = driverRuntimeRpcSchemas.driver.commandUpdate.input.safeParse({ + commandId: "command-1", + driverInstanceId: "driver-1", + result: { outputText: "partial", requestId: "request-1" }, + status: "completed", + }); + expect(commandUpdate.success).toBeFalse(); + expect(commandUpdate.error?.issues).toContainEqual( + expect.objectContaining({ path: ["result", "serverId"] }), + ); + + const events = driverRuntimeRpcSchemas.driver.pushEvents.input.safeParse({ + driverInstanceId: "driver-1", + events: [{}], + }); + expect(events.success).toBeFalse(); + expect(events.error?.issues[0]?.path).toEqual(["events", 0]); + + const command = driverRuntimeRpcSchemas.driverInstance.nextCommand.output.safeParse({ + command: { commandId: "command-1", kind: "unknown" }, + }); + expect(command.success).toBeFalse(); + expect(command.error?.issues[0]?.path).toEqual(["command"]); + }); + test("scanner carries only the suffix needed for cross-chunk detection", () => { const scanner = new ForbiddenSecretScanner(["sentinel-credential"]); expect(scanner.scan("noise-sentinel-")).toBe(false); diff --git a/tests/driver-artifact-test-controller.ts b/tests/driver-artifact-test-controller.ts index 8b6a931..4ed3c48 100644 --- a/tests/driver-artifact-test-controller.ts +++ b/tests/driver-artifact-test-controller.ts @@ -9,6 +9,9 @@ import { parseDriverCommandUpdateInput, parseDriverCompletionInput, parseDriverEventBatchInput, + parseDriverExternalToolEffectClaimInput, + parseDriverExternalToolEffectCompleteInput, + parseDriverExternalToolEffectUnknownInput, parseDriverFailureInput, parseDriverHeartbeatInput, parseDriverHelloInput, @@ -1003,10 +1006,8 @@ export class DriverArtifactTestController { return { ok: true }; } case "/driver/claimExternalToolEffect": { - const commandId = readString(input, "commandId", "external tool effect claim"); - this.#assertDriverInstanceId( - readString(input, "driverInstanceId", "external tool effect claim"), - ); + const { commandId, driverInstanceId } = parseDriverExternalToolEffectClaimInput(input); + this.#assertDriverInstanceId(driverInstanceId); const effectId = `artifact-test-effect-${commandId}`; const effect = this.#externalToolEffects.get(commandId); @@ -1021,31 +1022,13 @@ export class DriverArtifactTestController { return { attempt: 1, effectId, idempotencyKey: effectId, kind: "execute" }; } case "/driver/completeExternalToolEffect": { - const commandId = readString(input, "commandId", "external tool effect completion"); - const driverInstanceId = readString( - input, - "driverInstanceId", - "external tool effect completion", - ); + const { commandId, driverInstanceId, result } = + parseDriverExternalToolEffectCompleteInput(input); this.#assertDriverInstanceId(driverInstanceId); - const parsed = parseDriverCommandUpdateInput({ - commandId, - driverInstanceId, - result: input["result"], - status: "completed", - }); - - if ( - parsed.result === undefined || - parsed.result === null || - !("outputText" in parsed.result) - ) { - throw new Error("External tool effect completion requires an MCP result."); - } const current = this.#externalToolEffects.get(commandId); if (current?.status === "completed") { - if (JSON.stringify(current.result) !== JSON.stringify(parsed.result)) { + if (JSON.stringify(current.result) !== JSON.stringify(result)) { throw new Error(`External tool effect ${commandId} completed with a different result.`); } return { ok: true }; @@ -1055,16 +1038,14 @@ export class DriverArtifactTestController { } this.#externalToolEffects.set(commandId, { - result: structuredClone(parsed.result), + result: structuredClone(result), status: "completed", }); return { ok: true }; } case "/driver/markExternalToolEffectUnknown": { - const commandId = readString(input, "commandId", "external tool effect uncertainty"); - this.#assertDriverInstanceId( - readString(input, "driverInstanceId", "external tool effect uncertainty"), - ); + const { commandId, driverInstanceId } = parseDriverExternalToolEffectUnknownInput(input); + this.#assertDriverInstanceId(driverInstanceId); const current = this.#externalToolEffects.get(commandId); if (current?.status === "completed") { diff --git a/tests/driver-boot-schema.test.ts b/tests/driver-boot-schema.test.ts new file mode 100644 index 0000000..08ef117 --- /dev/null +++ b/tests/driver-boot-schema.test.ts @@ -0,0 +1,270 @@ +import { describe, expect, test } from "bun:test"; + +import { parseDriverBootPayload } from "../src/protocol/boot"; +import { parseDriverNativeRuntimeRef } from "../src/protocol/runtime"; +import { mergeProviderOptions } from "../src/runtimes/provider-options"; +import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; + +describe("Driver boot schema", () => { + test("uses the runtime parser for native refs and ignores inherited fields", () => { + const parsed = parseDriverNativeRuntimeRef({ + kind: "openai_thread_id", + runtimeId: "openai-runtime", + unknown: true, + value: "thread-1", + }); + + expect(parsed).toEqual({ + kind: "openai_thread_id", + runtimeId: "openai-runtime", + value: "thread-1", + }); + expect(() => + parseDriverNativeRuntimeRef( + Object.assign(Object.create({ kind: "openai_thread_id" }), { + runtimeId: "openai-runtime", + value: "thread-1", + }), + ), + ).toThrow(TypeError); + + expect(() => + parseDriverNativeRuntimeRef({ + get kind() { + throw new Error("getter must stay inside the parser boundary"); + }, + runtimeId: "openai-runtime", + value: "thread-1", + }), + ).toThrow(TypeError); + }); + + test("strips unknown fields and canonicalizes IDs", () => { + const ignoredCycle: { self?: unknown } = {}; + ignoredCycle.self = ignoredCycle; + const input = { + ...driverBootPayload, + driverInstanceId: driverBootPayload.driverInstanceId.toLowerCase(), + execution: { ...driverBootPayload.execution, unknownExecutionField: true }, + ignoredCycle, + unknownRootField: true, + }; + Object.defineProperty(input, "ignoredGetter", { + enumerable: true, + get: () => { + throw new Error("unknown fields must not be read"); + }, + }); + const parsed = parseDriverBootPayload(input); + + expect(parsed.driverInstanceId).toBe(driverBootPayload.driverInstanceId); + expect(parsed).not.toHaveProperty("unknownRootField"); + expect(parsed.execution).not.toHaveProperty("unknownExecutionField"); + }); + + test("requires the native resume kind to match its runtime", () => { + expect(() => + parseDriverBootPayload({ + ...driverBootPayload, + execution: { + ...driverBootPayload.execution, + session: { + ...driverBootPayload.execution.session, + nativeResumeRef: { + kind: "claude_session_id", + runtimeId: "openai-runtime", + value: "thread-1", + }, + }, + }, + }), + ).toThrow("does not match runtime openai-runtime"); + + expect(() => + parseDriverBootPayload({ + ...driverBootPayload, + execution: { + ...driverBootPayload.execution, + session: { + ...driverBootPayload.execution.session, + nativeResumeRef: { + kind: "claude_session_id", + runtimeId: "claude-agent-sdk", + value: "session-1", + }, + }, + }, + }), + ).toThrow("native resume runtime claude-agent-sdk does not match runtime openai-runtime"); + + expect(() => + parseDriverBootPayload({ + ...driverBootPayload, + runtimeTransport: "claude-agent-sdk", + }), + ).toThrow("runtime openai-runtime does not match transport claude-agent-sdk"); + }); + + test("preserves arbitrary JSON option keys and rejects non-JSON values", () => { + const providerOptions = JSON.parse('{"__proto__":{"enabled":true}}') as unknown; + const sparseOptions: unknown[] = []; + sparseOptions.length = 1; + const parsed = parseDriverBootPayload({ + ...driverBootPayload, + execution: { ...driverBootPayload.execution, providerOptions }, + }); + + expect(Object.hasOwn(parsed.execution.providerOptions, "__proto__")).toBe(true); + expect(parsed.execution.providerOptions["__proto__"]).toEqual({ enabled: true }); + + const merged = mergeProviderOptions({}, parsed.execution.providerOptions); + expect(Object.getPrototypeOf(merged)).toBe(Object.prototype); + expect(Object.hasOwn(merged, "__proto__")).toBe(true); + expect(merged).not.toHaveProperty("enabled"); + + expect(() => + parseDriverBootPayload({ + ...driverBootPayload, + execution: { ...driverBootPayload.execution, providerOptions: { invalid: Infinity } }, + }), + ).toThrow("must be JSON-serializable"); + + for (const invalid of [new Date(), new Map(), new Set(), sparseOptions]) { + expect(() => + parseDriverBootPayload({ + ...driverBootPayload, + execution: { ...driverBootPayload.execution, providerOptions: invalid }, + }), + ).toThrow(); + } + }); + + test("ignores inherited fields at every object boundary", () => { + const { bootToken: _bootToken, ...withoutBootToken } = driverBootPayload; + expect(() => + parseDriverBootPayload( + Object.assign(Object.create({ bootToken: "inherited" }), withoutBootToken), + ), + ).toThrow("bootToken"); + + const { model: _model, ...executionWithoutModel } = driverBootPayload.execution; + expect(() => + parseDriverBootPayload({ + ...driverBootPayload, + execution: Object.assign( + Object.create({ model: driverBootPayload.execution.model }), + executionWithoutModel, + ), + }), + ).toThrow("model"); + }); + + test("applies defaults while keeping absent optional fields absent", () => { + const { + providerOptions: _providerOptions, + session: originalSession, + ...execution + } = driverBootPayload.execution; + const { recoveryMessages: _recoveryMessages, ...session } = originalSession; + const parsed = parseDriverBootPayload({ + ...driverBootPayload, + execution: { + ...execution, + environment: { ...execution.environment, paths: undefined }, + permissionPolicy: null, + session: { + ...session, + mcpServers: [ + { + authType: "token", + authorizationState: "disabled", + credentialScope: "sandbox", + credentialStatus: "disabled", + name: "disabled-server", + serverId: DRIVER_TEST_IDS.agentId, + subjectLabel: undefined, + }, + ], + recoveryMessages: undefined, + }, + skillCatalog: [ + { + frontmatter: {}, + mountPath: "/skills/example", + resolutionMode: "explicit", + skillId: DRIVER_TEST_IDS.agentId, + skillName: "example", + }, + ], + skills: [ + { + archiveFormat: "zip", + blobSha256: "sha256", + compression: "deflate", + downloadUrl: "artifact://skill", + materializationStatus: "ready", + mountPath: "/skills/example", + resolutionMode: "explicit", + skillId: DRIVER_TEST_IDS.agentId, + skillName: "example", + snapshotId: undefined, + warningCode: undefined, + }, + ], + }, + }); + + expect(parsed.execution.permissionPolicy).toBe("full_access"); + expect(parsed.execution.providerOptions).toEqual({}); + expect(parsed.execution.session.recoveryMessages).toEqual([]); + expect(parsed.execution.environment).not.toHaveProperty("paths"); + expect(parsed.execution.session.mcpServers[0]).not.toHaveProperty("subjectLabel"); + expect(parsed.execution.skills[0]).not.toHaveProperty("snapshotId"); + expect(parsed.execution.skills[0]).not.toHaveProperty("warningCode"); + expect(parsed.execution.skillCatalog[0]?.frontmatter).toEqual({ + author: null, + description: null, + version: null, + }); + }); + + test("rejects array-shaped environment variables", () => { + expect(() => + parseDriverBootPayload({ + ...driverBootPayload, + execution: { + ...driverBootPayload.execution, + environment: { variables: [["NAME", "value"]] }, + }, + }), + ).toThrow("must be an object"); + }); + + test("rejects inherited sparse array entries", () => { + const directories: string[] = []; + directories.length = 1; + Array.prototype[0] = "/inherited"; + try { + expect(() => + parseDriverBootPayload({ + ...driverBootPayload, + execution: { + ...driverBootPayload.execution, + session: { ...driverBootPayload.execution.session, additionalDirectories: directories }, + }, + }), + ).toThrow(); + } finally { + delete Array.prototype[0]; + } + }); + + test("rejects unsupported control URL protocols", () => { + expect(() => + parseDriverBootPayload({ ...driverBootPayload, controlUrl: "file:///tmp/socket" }), + ).toThrow("must use http, https, ws, or wss"); + expect(() => parseDriverBootPayload({ ...driverBootPayload, controlUrl: "not-url" })).toThrow( + TypeError, + ); + }); +}); diff --git a/tests/driver-golden-fixtures.test.ts b/tests/driver-golden-fixtures.test.ts index 5bd800a..1a36b45 100644 --- a/tests/driver-golden-fixtures.test.ts +++ b/tests/driver-golden-fixtures.test.ts @@ -19,6 +19,7 @@ const commandFixtures = [ ] as const; const runtimeEventFixtures = [ + "agent-tasks-replaced", "diagnostic-reported", "message-delta", "permission-requested", @@ -134,6 +135,50 @@ describe("Driver golden fixtures", () => { } }); + test("enforces the agent task replacement envelope and payload contract", () => { + const context = createRuntimeEventContext(); + const event = { + delivery: "lossless" as const, + kind: "agent.tasks.replaced" as const, + payload: { + tasks: [{ taskId: "task-1", taskType: "local_agent", title: "Inspect repository" }], + }, + visibility: "participant" as const, + }; + + expect(ingestRuntimeEventInput(context, event)).toMatchObject({ status: "accepted" }); + + for (const input of [ + { ...event, delivery: "best_effort" }, + { ...event, visibility: "owner_debug" }, + { ...event, payload: {} }, + { ...event, payload: { tasks: [{ taskId: "" }] } }, + { ...event, payload: { tasks: [{ private: true, taskId: "task-1" }] } }, + { ...event, payload: { tasks: [{ taskId: "task-1" }, { taskId: "task-1" }] } }, + ]) { + expect(ingestRuntimeEventInput(context, input)).toMatchObject({ status: "rejected" }); + } + + expect( + ingestRuntimeEventInput({ ...context, driverInstanceId: undefined }, event), + ).toMatchObject({ status: "rejected" }); + expect(ingestRuntimeEventInput({ ...context, runId: undefined }, event)).toMatchObject({ + status: "rejected", + }); + + expect( + ingestRuntimeEventInput(context, { + ...event, + payload: { + tasks: Array.from({ length: 256 }, (_, index) => ({ + taskId: `task-${String(index)}`, + title: "界".repeat(4_096), + })), + }, + }), + ).toMatchObject({ status: "rejected" }); + }); + test("bounds control reasons without dropping their commands", () => { const limit = "x".repeat(16 * 1_024); const oversized = `${limit}界`; diff --git a/tests/driver-instance-socket-socket.test.ts b/tests/driver-instance-socket-socket.test.ts index 951e0ea..60a865b 100644 --- a/tests/driver-instance-socket-socket.test.ts +++ b/tests/driver-instance-socket-socket.test.ts @@ -55,6 +55,7 @@ class RpcWebSocket extends OpenWebSocket { static lostResponsePath: string | null = null; static nextCommand: unknown = null; static receiptOverride: Partial<{ eventId: string; seq: number; type: string }> | null = null; + static responseOverrides = new Map(); static sendFailurePath: string | null = null; static stalledPath: string | null = null; static stalled = Promise.withResolvers(); @@ -148,6 +149,10 @@ class RpcWebSocket extends OpenWebSocket { output = { ok: true }; } + if (RpcWebSocket.responseOverrides.has(path)) { + output = RpcWebSocket.responseOverrides.get(path); + } + queueMicrotask(() => { this.dispatchEvent( new MessageEvent("message", { @@ -184,6 +189,7 @@ afterEach(() => { RpcWebSocket.lostResponsePath = null; RpcWebSocket.nextCommand = null; RpcWebSocket.receiptOverride = null; + RpcWebSocket.responseOverrides.clear(); RpcWebSocket.sendFailurePath = null; RpcWebSocket.stalledPath = null; RpcWebSocket.stalled = Promise.withResolvers(); @@ -457,6 +463,48 @@ describe("DriverInstanceSocket lifecycle", () => { ]); }); + test("validates control-plane responses before returning them", async () => { + const socket = await connectRpcSocket(); + const signal = new AbortController().signal; + + RpcWebSocket.responseOverrides.set("/driver/heartbeat", { + heartbeatCount: 1, + ok: false, + }); + await expect( + socket.heartbeat({ at: new Date(0).toISOString(), reason: "interval" }), + ).rejects.toThrow("expected true"); + + RpcWebSocket.responseOverrides.set("/driver/ready", { ok: false }); + await expect(socket.ready({ at: new Date(0).toISOString() })).rejects.toThrow("expected true"); + + RpcWebSocket.responseOverrides.set("/driver/claimExternalToolEffect", { + attempt: 0, + effectId: "effect-1", + idempotencyKey: "effect-1", + kind: "execute", + }); + await expect( + socket.claimExternalToolEffect({ commandId: "command-1" }, signal), + ).rejects.toThrow("attempt must be a positive safe integer"); + }); + + test("marks a run terminal delivered only after validating its response", async () => { + const socket = await connectRpcSocket(); + socket.beginRun(DRIVER_TEST_IDS.runId); + RpcWebSocket.responseOverrides.set("/driver/completeRun", { ok: false }); + + await expect(socket.completeRun()).rejects.toThrow("expected true"); + RpcWebSocket.responseOverrides.delete("/driver/completeRun"); + await expect(socket.completeRun()).resolves.toBeUndefined(); + + expect( + (PendingWebSocket.instances[0] as RpcWebSocket).paths.filter( + (path) => path === "/driver/completeRun", + ), + ).toHaveLength(2); + }); + test("uses the terminal-attempt signal to abort only that command update", async () => { globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; RpcWebSocket.stalledPath = "/driver/commandUpdate"; @@ -646,6 +694,33 @@ describe("DriverInstanceSocket lifecycle", () => { expect(secondWire.paths.filter((sent) => sent === path)).toHaveLength(1); }); + test("rejects event receipts that are not a submitted-prefix", async () => { + const socket = await connectRpcSocket(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + RpcWebSocket.responseOverrides.set("/driver/pushEvents", { + accepted: [ + { seq: 1, type: "message.completed" }, + { seq: 2, type: "message.completed" }, + ], + }); + + await expect( + socket.pushEvents({ + events: [ + { + kind: "message.completed", + payload: { messageId: "message-1", stopReason: "end_turn" }, + }, + ], + }), + ).rejects.toThrow("receipt count exceeds"); + }); + test("splits event delivery at the negotiated batch limit", async () => { const socket = await connectRpcSocket(); await socket.hello({ diff --git a/tests/fixtures/architecture-boundaries-audit.ts b/tests/fixtures/architecture-boundaries-audit.ts deleted file mode 100644 index c05740f..0000000 --- a/tests/fixtures/architecture-boundaries-audit.ts +++ /dev/null @@ -1,22 +0,0 @@ -declare const moduleName: string; - -void import("./" + moduleName); -void require("node:fs"); -void ((load) => load("node:fs"))(require); -void import.meta.require("node:fs"); -void import.meta["require"]("node:fs"); -void module.exports; -void exports.value; -void require; -void module; -void exports; -void process.getBuiltinModule("node:module"); -void process["getBuiltinModule"]("module"); - -// @ts-expect-error -- Exercises the CommonJS syntax guard. -import legacy = require("node:fs"); -// @ts-expect-error -- Exercises the CommonJS syntax guard. -export = legacy; - -import "node:module"; -import "module"; diff --git a/tests/fixtures/architecture-boundaries-resolution.ts b/tests/fixtures/architecture-boundaries-resolution.ts deleted file mode 100644 index a634aaa..0000000 --- a/tests/fixtures/architecture-boundaries-resolution.ts +++ /dev/null @@ -1,25 +0,0 @@ -// oxlint-disable-next-line typescript/triple-slash-reference -- Exercises dependency auditing. -/// - -import packageMetadata from "../../package.json" with { type: "json" }; -import "../../src/paths.js"; -import type { Module } from "node:module"; - -export type { Command } from "../../src/contract/index.js"; -export type { Module as ExportedNodeModule } from "node:module"; -export type ImportedNodeModule = Module; -export const moduleUrl = import.meta.url; - -const require = () => packageMetadata.name; -const module = packageMetadata.name; -const exports = packageMetadata.name; -export const process = { getBuiltinModule: (_name: string) => packageMetadata.name }; - -void packageMetadata; -void require(); -void module; -void exports; -void process.getBuiltinModule("node:module"); -void { exports: packageMetadata.name, module, require }; - -declare module "../../src/core/agent-driver-kernel.ts" {} diff --git a/tests/fixtures/architecture-boundaries-runtime-loaders.ts b/tests/fixtures/architecture-boundaries-runtime-loaders.ts deleted file mode 100644 index 9be9de9..0000000 --- a/tests/fixtures/architecture-boundaries-runtime-loaders.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getBuiltinModule as importedLoader } from "node:process"; - -import { process as localProcess } from "./architecture-boundaries-resolution.js"; - -const { getBuiltinModule: destructuredLoader } = process; -const builtinModuleKey = "getBuiltinModule" as const; - -export function direct(): void { - void process.getBuiltinModule("node:module"); -} - -export function imported(): void { - void importedLoader("node:module"); -} - -export function destructured(): void { - void destructuredLoader("node:module"); -} - -export function computed(): void { - void process[builtinModuleKey]("node:module"); -} - -export function local(): void { - void localProcess.getBuiltinModule("node:module"); -} diff --git a/tests/fixtures/driver/runtime-event-drafts/agent-tasks-replaced.json b/tests/fixtures/driver/runtime-event-drafts/agent-tasks-replaced.json new file mode 100644 index 0000000..f60465b --- /dev/null +++ b/tests/fixtures/driver/runtime-event-drafts/agent-tasks-replaced.json @@ -0,0 +1,14 @@ +{ + "delivery": "lossless", + "kind": "agent.tasks.replaced", + "payload": { + "tasks": [ + { + "taskId": "task-1", + "taskType": "local_agent", + "title": "Inspect the repository" + } + ] + }, + "visibility": "participant" +} diff --git a/tests/fixtures/driver/runtime-event-envelopes/agent-tasks-replaced.json b/tests/fixtures/driver/runtime-event-envelopes/agent-tasks-replaced.json new file mode 100644 index 0000000..be4c92a --- /dev/null +++ b/tests/fixtures/driver/runtime-event-envelopes/agent-tasks-replaced.json @@ -0,0 +1,24 @@ +{ + "actor": "driver", + "delivery": "lossless", + "driverInstanceId": "01J0000000000000000000000F", + "id": "01J0000000000000000000000G", + "kind": "agent.tasks.replaced", + "occurredAt": "2026-05-26T00:00:00.000Z", + "origin": "driver", + "payload": { + "tasks": [ + { + "taskId": "task-1", + "taskType": "local_agent", + "title": "Inspect the repository" + } + ] + }, + "runId": "01J00000000000000000000012", + "runtimeId": "runtime-1", + "schemaVersion": "2026-05-26", + "sessionId": "01J00000000000000000000008", + "traceId": "trace-1", + "visibility": "participant" +} diff --git a/tests/fixtures/providers/acp/cases/permission-request.json b/tests/fixtures/providers/acp/cases/permission-request.json index 8568a9c..02e0cd1 100644 --- a/tests/fixtures/providers/acp/cases/permission-request.json +++ b/tests/fixtures/providers/acp/cases/permission-request.json @@ -32,6 +32,7 @@ "stopReason": "end_turn", "usage": null }, + "updates": [], "expectedEvents": [ { "kind": "message.started", diff --git a/tests/fixtures/providers/acp/cases/session-ready.json b/tests/fixtures/providers/acp/cases/session-ready.json index b743585..1c058cb 100644 --- a/tests/fixtures/providers/acp/cases/session-ready.json +++ b/tests/fixtures/providers/acp/cases/session-ready.json @@ -20,6 +20,7 @@ } } }, + "updates": [], "expectedEvents": [ { "kind": "session.created", diff --git a/tests/fixtures/providers/claude-agent-sdk/cases/result-failure-diagnostic.json b/tests/fixtures/providers/claude-agent-sdk/cases/result-failure-diagnostic.json index 05908f3..5a41b29 100644 --- a/tests/fixtures/providers/claude-agent-sdk/cases/result-failure-diagnostic.json +++ b/tests/fixtures/providers/claude-agent-sdk/cases/result-failure-diagnostic.json @@ -72,6 +72,14 @@ "used": null } }, + { + "delivery": "lossless", + "kind": "agent.tasks.replaced", + "payload": { + "tasks": [] + }, + "visibility": "participant" + }, { "kind": "run.failed", "payload": { diff --git a/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json b/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json index 74b6c08..1025313 100644 --- a/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json +++ b/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json @@ -227,6 +227,14 @@ "role": "agent" } }, + { + "delivery": "lossless", + "kind": "agent.tasks.replaced", + "payload": { + "tasks": [] + }, + "visibility": "participant" + }, { "kind": "run.completed", "payload": { diff --git a/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json b/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json index dd1c01d..73b7b2a 100644 --- a/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json +++ b/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json @@ -47,6 +47,13 @@ }, "sourceEventId": "openai.turn.started:turn-1" }, + { + "delivery": "lossless", + "kind": "agent.tasks.replaced", + "payload": { "tasks": [] }, + "sourceEventId": "openai.turn.failed.closure:turn-1:0", + "visibility": "participant" + }, { "kind": "run.failed", "native": { diff --git a/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json b/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json index 6837e3f..23cbe96 100644 --- a/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json +++ b/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json @@ -254,6 +254,14 @@ "payload": { "resumePointer": "thread-1", "threadId": "thread-1" }, "visibility": "owner_debug" }, + { + "delivery": "lossless", + "kind": "agent.tasks.replaced", + "payload": { "tasks": [] }, + "runId": "", + "sourceEventId": "openai.turn.completed.closure:turn-root:0", + "visibility": "participant" + }, { "kind": "run.completed", "native": { diff --git a/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json b/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json index f7f86c8..f2f2059 100644 --- a/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json +++ b/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json @@ -69,6 +69,13 @@ }, "visibility": "owner_debug" }, + { + "delivery": "lossless", + "kind": "agent.tasks.replaced", + "payload": { "tasks": [] }, + "sourceEventId": "openai.turn.completed.closure:turn-1:0", + "visibility": "participant" + }, { "kind": "run.completed", "native": { diff --git a/tests/fixtures/public-api-spaces-source.ts b/tests/fixtures/public-api-spaces-source.ts deleted file mode 100644 index 404a426..0000000 --- a/tests/fixtures/public-api-spaces-source.ts +++ /dev/null @@ -1,16 +0,0 @@ -export class AuditBoth {} -export interface AuditType { - readonly value: string; -} -export const AuditValue = 1; - -export function AuditMerged(): void {} -export declare namespace AuditMerged { - type Member = string; -} - -export declare namespace AuditDeclaredNamespace { - type Member = string; -} - -export default class AuditDefault {} diff --git a/tests/fixtures/public-api-type-namespace.ts b/tests/fixtures/public-api-type-namespace.ts deleted file mode 100644 index 525fbd2..0000000 --- a/tests/fixtures/public-api-type-namespace.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare namespace Named { - type Member = string; -} - -export type { Named }; -export * from "./public-api-spaces-source.ts"; -export { default, default as AuditDefault } from "./public-api-spaces-source.ts"; -export * as AuditNamespace from "./public-api-spaces-source.ts"; -export type * as AuditTypeNamespace from "./public-api-spaces-source.ts"; -export type * from "./public-api-type-star-source.ts"; diff --git a/tests/fixtures/public-api-type-star-source.ts b/tests/fixtures/public-api-type-star-source.ts deleted file mode 100644 index e959eea..0000000 --- a/tests/fixtures/public-api-type-star-source.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class TypeStarBoth {} -export interface TypeStarOnly { - readonly value: string; -} -export const TypeStarValue = 1; diff --git a/tests/openai-app-server-agent-task-events.test.ts b/tests/openai-app-server-agent-task-events.test.ts new file mode 100644 index 0000000..88b96a4 --- /dev/null +++ b/tests/openai-app-server-agent-task-events.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, test } from "bun:test"; + +import type { DriverEventInput } from "../src/protocol/events"; +import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-event-envelope"; +import { + OpenAiAgentTaskState, + openAiAgentTasksClosedEvent, + type OpenAiSubAgentActivity, +} from "../src/runtimes/openai/app-server-agent-task-events"; +import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; + +function apply(state: OpenAiAgentTaskState, activity: OpenAiSubAgentActivity) { + const update = state.prepare(activity); + + for (const event of update.events) { + toDriverEventEnvelopes(driverBootPayload, event, DRIVER_TEST_IDS.runId); + } + update.commit(); + return update.events; +} + +function tasks(events: readonly DriverEventInput[]) { + const snapshot = events.find((event) => event.kind === "agent.tasks.replaced"); + + if (snapshot === undefined) { + throw new Error("Expected an OpenAI agent task snapshot."); + } + + return (snapshot.payload as { tasks: unknown[] }).tasks; +} + +describe("OpenAI app-server agent task snapshots", () => { + test("projects interleaved completion-only activity as full active-set replacements", () => { + const state = new OpenAiAgentTaskState(); + const activity = ( + agentId: string, + kind: OpenAiSubAgentActivity["kind"], + ): OpenAiSubAgentActivity => ({ agentId, agentPath: `/root/${agentId}`, kind }); + + expect(tasks(apply(state, activity("agent-1", "started")))).toEqual([ + { taskId: "agent-1", taskType: "openai_subagent", title: "/root/agent-1" }, + ]); + expect(tasks(apply(state, activity("agent-2", "interacted")))).toHaveLength(2); + expect(tasks(apply(state, activity("agent-1", "started")))).toHaveLength(2); + expect(tasks(apply(state, activity("agent-1", "completed")))).toEqual([ + { taskId: "agent-2", taskType: "openai_subagent", title: "/root/agent-2" }, + ]); + expect(tasks(apply(state, activity("agent-1", "started")))).toEqual([ + { taskId: "agent-2", taskType: "openai_subagent", title: "/root/agent-2" }, + ]); + expect(tasks(apply(state, activity("agent-1", "interacted")))).toHaveLength(2); + expect(tasks(apply(state, activity("agent-1", "completed")))).toEqual([ + { taskId: "agent-2", taskType: "openai_subagent", title: "/root/agent-2" }, + ]); + expect(tasks(apply(state, activity("agent-2", "interrupted")))).toEqual([]); + + state.reset(); + expect(tasks(apply(state, activity("agent-1", "interacted")))).toHaveLength(1); + }); + + test("keeps provider data bounded without truncating active membership", () => { + const state = new OpenAiAgentTaskState(); + const privateMarker = `private-prompt-${"x".repeat(4_096)}`; + let events: DriverEventInput[] = []; + + for (let index = 0; index < 256; index += 1) { + events = apply(state, { + agentId: `agent-${String(index)}`, + agentPath: `${privateMarker}-${String(index)}`, + kind: "started", + }); + } + + expect(events).toContainEqual( + expect.objectContaining({ + kind: "diagnostic.reported", + payload: expect.objectContaining({ code: "openai.agent_tasks_snapshot_too_large" }), + }), + ); + expect(tasks(events)).toEqual( + Array.from({ length: 256 }, (_, index) => ({ taskId: `agent-${String(index)}` })), + ); + expect(JSON.stringify(events)).not.toContain(privateMarker); + }); + + test("recovers an authoritative snapshot after the active set returns below 257", () => { + const state = new OpenAiAgentTaskState(); + + for (let index = 0; index < 256; index += 1) { + apply(state, { + agentId: `agent-${String(index)}`, + agentPath: `/root/agent-${String(index)}`, + kind: "started", + }); + } + + const overflow = apply(state, { + agentId: "agent-256", + agentPath: "/root/agent-256", + kind: "started", + }); + expect(overflow).toEqual([ + expect.objectContaining({ + kind: "diagnostic.reported", + payload: expect.objectContaining({ code: "openai.visible_agent_tasks_too_many" }), + }), + ]); + + const recovered = tasks( + apply(state, { + agentId: "agent-0", + agentPath: "/root/agent-0", + kind: "completed", + }), + ); + expect(recovered).toHaveLength(256); + expect(recovered).not.toContainEqual(expect.objectContaining({ taskId: "agent-0" })); + expect(recovered).toContainEqual(expect.objectContaining({ taskId: "agent-256" })); + }); + + test("fails at the bounded active activity-state limit", () => { + const state = new OpenAiAgentTaskState(); + + for (let index = 0; index < 1_024; index += 1) { + apply(state, { + agentId: `agent-${String(index)}`, + agentPath: `/root/agent-${String(index)}`, + kind: "started", + }); + } + + expect(() => + state.prepare({ agentId: "agent-1024", agentPath: "/root/overflow", kind: "started" }), + ).toThrow("exceeds 1024"); + }); + + test("bounds completed replay protection without letting late starts revive", () => { + const state = new OpenAiAgentTaskState(); + + for (let index = 0; index < 1_024; index += 1) { + apply(state, { + agentId: `closed-${String(index)}`, + agentPath: `/root/closed-${String(index)}`, + kind: "completed", + }); + } + for (let index = 0; index < 1_024; index += 1) { + expect( + tasks( + apply(state, { + agentId: `closed-${String(index)}`, + agentPath: `/root/closed-${String(index)}`, + kind: "started", + }), + ), + ).toEqual([]); + } + + expect(() => + state.prepare({ + agentId: "closed-1024", + agentPath: "/root/closed-1024", + kind: "completed", + }), + ).toThrow("closed sub-agent count exceeds 1024"); + expect( + tasks( + apply(state, { + agentId: "closed-0", + agentPath: "/root/closed-0", + kind: "started", + }), + ), + ).toEqual([]); + }); + + test("uses deterministic bounded IDs and an authoritative empty terminal snapshot", () => { + const state = new OpenAiAgentTaskState(); + const longId = "agent".repeat(100); + const first = tasks( + apply(state, { agentId: longId, agentPath: `${"a".repeat(4_095)}😀`, kind: "started" }), + ); + + state.reset(); + const replay = tasks( + apply(state, { agentId: longId, agentPath: "/root/replayed", kind: "started" }), + ); + expect(first[0]).toMatchObject({ taskId: expect.stringMatching(/^openai-task:[\da-f]{64}$/) }); + expect(replay[0]).toMatchObject({ taskId: (first[0] as { taskId: string }).taskId }); + expect((first[0] as { title: string }).title).toHaveLength(4_095); + expect(openAiAgentTasksClosedEvent()).toEqual({ + delivery: "lossless", + kind: "agent.tasks.replaced", + payload: { tasks: [] }, + visibility: "participant", + }); + }); +}); diff --git a/tests/openai-app-server-event-bridge-items.test.ts b/tests/openai-app-server-event-bridge-items.test.ts index 3ae2927..cf39063 100644 --- a/tests/openai-app-server-event-bridge-items.test.ts +++ b/tests/openai-app-server-event-bridge-items.test.ts @@ -125,6 +125,7 @@ describe("OpenAi app-server event bridge", () => { { kind: "message.started" }, { kind: "item.started" }, { kind: "tool.call.updated", payload: { status: "running", toolCallId: "tool-1" } }, + { kind: "agent.tasks.replaced", payload: { tasks: [] } }, { kind: "run.cancel.requested", runId: DRIVER_TEST_IDS.runId }, { kind: "message.cancelled" }, { kind: "tool.call.updated", payload: { status: "cancelled", toolCallId: "tool-1" } }, @@ -402,6 +403,36 @@ describe("OpenAi app-server event bridge", () => { ).toHaveLength(2); }); + test("replays a full task snapshot before committing its active set", async () => { + const harness = createHarness({ failReasonOnce: "driver.openai.item.completed" }); + const notification = { + item: { + agentPath: "/root/worker", + agentThreadId: "agent-1", + id: "activity-1", + kind: "started", + type: "subAgentActivity", + }, + threadId: "thread-1", + turnId: "turn-1", + } as const; + + await expect( + harness.bridge.handleNotification(harness.context, "item/completed", notification), + ).rejects.toThrow("first attempt"); + await harness.bridge.handleNotification(harness.context, "item/completed", notification); + await harness.bridge.handleNotification(harness.context, "item/completed", notification); + + const attempts = harness.attempts.filter( + ({ reason }) => reason === "driver.openai.item.completed", + ); + expect(attempts).toHaveLength(2); + expect(attempts[0]!.events).toEqual(attempts[1]!.events); + expect(harness.events().filter((event) => event.kind === "agent.tasks.replaced")).toMatchObject( + [{ payload: { tasks: [{ taskId: "agent-1" }] } }], + ); + }); + test("replays a plan delta after delivery rejection without duplicating content", async () => { const harness = createHarness({ failReasonOnce: "driver.openai.plan.delta" }); const notification = { diff --git a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts index e4cbda6..93c3716 100644 --- a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts +++ b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts @@ -331,6 +331,10 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", }, }, + { + kind: "agent.tasks.replaced", + payload: { tasks: [] }, + }, { kind: "run.completed", payload: { diff --git a/tests/openai-app-server-event-bridge-terminal.test.ts b/tests/openai-app-server-event-bridge-terminal.test.ts index 96ec863..9b5a67f 100644 --- a/tests/openai-app-server-event-bridge-terminal.test.ts +++ b/tests/openai-app-server-event-bridge-terminal.test.ts @@ -1827,6 +1827,10 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", }, }, + { + kind: "agent.tasks.replaced", + payload: { tasks: [] }, + }, { kind: "run.completed", payload: { diff --git a/tests/openai-app-server-event-bridge-transcript.test.ts b/tests/openai-app-server-event-bridge-transcript.test.ts index 1a339f1..dfe7531 100644 --- a/tests/openai-app-server-event-bridge-transcript.test.ts +++ b/tests/openai-app-server-event-bridge-transcript.test.ts @@ -213,6 +213,10 @@ describe("OpenAi app-server event bridge", () => { threadId: "thread-1", }, }, + { + kind: "agent.tasks.replaced", + payload: { tasks: [] }, + }, { kind: "run.completed", payload: { @@ -282,6 +286,10 @@ describe("OpenAi app-server event bridge", () => { { kind: "run.started", }, + { + kind: "agent.tasks.replaced", + payload: { tasks: [] }, + }, { kind: "run.failed", payload: { diff --git a/tests/openai-app-server-provider-fixtures.test.ts b/tests/openai-app-server-provider-fixtures.test.ts index 0695c0a..7b45835 100644 --- a/tests/openai-app-server-provider-fixtures.test.ts +++ b/tests/openai-app-server-provider-fixtures.test.ts @@ -1133,6 +1133,24 @@ describe("OpenAI app-server provider fixtures", () => { }, sourceEventId: `openai.item.completed:turn-1:activity-${activityKind}:0`, }, + { + delivery: "lossless", + kind: "agent.tasks.replaced", + payload: { + tasks: + activityKind === "started" || activityKind === "interacted" + ? [ + { + taskId: "agent-1", + taskType: "openai_subagent", + title: "/root/worker", + }, + ] + : [], + }, + sourceEventId: `openai.item.completed:turn-1:activity-${activityKind}:1`, + visibility: "participant", + }, ]); }, ); @@ -1176,6 +1194,14 @@ describe("OpenAI app-server provider fixtures", () => { }, sourceEventId: "openai.item.completed:turn-snapshot:activity-replayed:0", }); + const snapshots = events().filter((event) => event.kind === "agent.tasks.replaced"); + expect(snapshots).toMatchObject([ + { payload: { tasks: [{ taskId: "agent-replayed" }] } }, + { delivery: "lossless", payload: { tasks: [] }, visibility: "participant" }, + ]); + expect(events().findIndex((event) => event.kind === "agent.tasks.replaced")).toBeLessThan( + events().findIndex((event) => event.kind === "run.completed"), + ); }); test.each([ diff --git a/tests/openai-app-server-startup.test.ts b/tests/openai-app-server-startup.test.ts index c8d7ff1..651c775 100644 --- a/tests/openai-app-server-startup.test.ts +++ b/tests/openai-app-server-startup.test.ts @@ -1088,6 +1088,7 @@ describe("OpenAI app-server startup", () => { ["message.started", undefined], ["item.started", undefined], ["tool.call.updated", "running"], + ["agent.tasks.replaced", undefined], ["run.started", undefined], ["run.cancel.requested", undefined], ["message.cancelled", undefined], @@ -1457,7 +1458,7 @@ describe("OpenAI app-server startup", () => { harness.events .filter((event) => event.runId === DRIVER_TEST_IDS.runId) .map((event) => event.kind), - ).toEqual(["run.started", "run.failed"]); + ).toEqual(["agent.tasks.replaced", "run.started", "run.failed"]); } finally { await harness.releaseTurnStartResponse(); await harness.backend @@ -1537,7 +1538,7 @@ describe("OpenAI app-server startup", () => { harness.events .filter((event) => event.runId === DRIVER_TEST_IDS.runId) .map((event) => event.kind), - ).toEqual(["run.started", "run.failed"]); + ).toEqual(["agent.tasks.replaced", "run.started", "run.failed"]); const terminal = harness.events.find((event) => event.kind === "run.failed"); expect(terminal).toMatchObject({ payload: { diff --git a/tests/opencode-acp-contract.test.ts b/tests/opencode-acp-contract.test.ts index 718be6c..bc621ba 100644 --- a/tests/opencode-acp-contract.test.ts +++ b/tests/opencode-acp-contract.test.ts @@ -176,6 +176,7 @@ test("OpenCode rejects unadvertised additional directories before session creati session: { ...driverStartInput.execution.session, additionalDirectories: [additionalDirectory], + context: sessionContext, cwd, homePath, sharedRootPath: cwd, @@ -190,7 +191,6 @@ test("OpenCode rejects unadvertised additional directories before session creati connection: connection.agent, currentSessionId: null, payload, - sessionContext, replaySession: async (operation) => operation(), }), ).rejects.toThrow("does not advertise additionalDirectories support"); diff --git a/tests/protocol-id.test.ts b/tests/protocol-id.test.ts new file mode 100644 index 0000000..9158670 --- /dev/null +++ b/tests/protocol-id.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test"; + +import { isDriverId, normalizeDriverId, parseDriverId } from "../src/protocol/id"; + +describe("Driver IDs", () => { + test("accepts the full ULID range and canonicalizes lowercase input", () => { + const maximum = "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"; + + expect(parseDriverId(maximum)).toBe(maximum); + expect(normalizeDriverId(maximum.toLowerCase())).toBe(maximum); + expect(isDriverId(maximum)).toBe(true); + }); + + test.each(["80000000000000000000000000", "Z0000000000000000000000000"])( + "rejects an overflowing ULID %s", + (value) => { + expect(() => parseDriverId(value)).toThrow("must be a valid ULID"); + expect(isDriverId(value)).toBe(false); + }, + ); +}); diff --git a/tests/provider-fixture-test-helpers.ts b/tests/provider-fixture-test-helpers.ts index b43d461..632acb4 100644 --- a/tests/provider-fixture-test-helpers.ts +++ b/tests/provider-fixture-test-helpers.ts @@ -24,7 +24,7 @@ export function readProviderFixture( return value as T; } -type ProviderFixtureKind = "claude" | "openai"; +type ProviderFixtureKind = "acp" | "claude" | "openai"; function collectDriverIds( value: unknown, @@ -107,7 +107,7 @@ function normalizeValue( const entries = Object.entries(value); return Object.fromEntries( - provider === "claude" + provider !== "openai" ? entries.flatMap(([key, entry]) => entry === undefined ? [] : [[key, normalizeValue(entry, aliases, provider, key)]], ) @@ -122,11 +122,25 @@ function normalizeProviderEvents( const aliases = new Map(); for (const event of events) { - collectDriverIds(event, aliases, provider); + if (provider === "acp") { + const payload = isRecord(event.payload) ? event.payload : null; + const messageId = payload?.["messageId"]; + + if ( + event.kind === "message.started" && + payload?.["role"] === "agent" && + typeof messageId === "string" && + !aliases.has(messageId) + ) { + aliases.set(messageId, `assistant-message-${aliases.size + 1}`); + } + } else { + collectDriverIds(event, aliases, provider); + } } return events.map((event) => { - if (provider === "claude") { + if (provider !== "openai") { return normalizeValue(event, aliases, provider) as Record; } @@ -146,6 +160,12 @@ function normalizeProviderEvents( }); } +export function normalizeAcpProviderEvents( + events: readonly DriverEventInput[], +): Record[] { + return normalizeProviderEvents(events, "acp"); +} + export function normalizeClaudeProviderEvents( events: readonly DriverEventInput[], ): Record[] { diff --git a/tests/public-api-consumer.ts b/tests/public-api-consumer.ts index 0c86e42..97c0cec 100644 --- a/tests/public-api-consumer.ts +++ b/tests/public-api-consumer.ts @@ -4,18 +4,29 @@ import type { CmaStore, DriverStartInput, } from "@mosoo/agent-driver"; +import type { DriverBootPayload } from "@mosoo/agent-driver/boot"; import type { SessionSnapshot } from "@mosoo/agent-driver/contract"; import type { CmaHttpHandler } from "@mosoo/agent-driver/cma-http"; import type { CmaSdkClient } from "@mosoo/agent-driver/cma-sdk"; import type { DriverEventInput } from "@mosoo/agent-driver/events"; +import type { DriverHeartbeatInput } from "@mosoo/agent-driver/orpc"; +import type { OpenAiPrivateCitationFilterResult } from "@mosoo/agent-driver/provider-output"; +import type { DriverRuntime } from "@mosoo/agent-driver/runtime"; + +type SandboxMemoryPath = typeof import("@mosoo/agent-driver/paths").SANDBOX_MEMORY_PATH; export interface PublicApiConsumer { readonly backend: AgentDriverBackend; + readonly bootPayload: DriverBootPayload; readonly cmaClient: CmaSdkClient; readonly cmaHandler: CmaHttpHandler; readonly cmaStore: CmaStore; readonly event: DriverEventInput; + readonly heartbeat: DriverHeartbeatInput; readonly kernel: AgentDriverKernel; + readonly memoryPath: SandboxMemoryPath; + readonly providerOutput: OpenAiPrivateCitationFilterResult; + readonly runtime: DriverRuntime; readonly snapshot: SessionSnapshot; readonly startInput: DriverStartInput; } diff --git a/tests/public-api-exports.snapshot.json b/tests/public-api-exports.snapshot.json deleted file mode 100644 index 78441bc..0000000 --- a/tests/public-api-exports.snapshot.json +++ /dev/null @@ -1,563 +0,0 @@ -{ - ".": { - "types": [ - "AgentDriverBackend", - "AgentDriverBackendFactory", - "AgentDriverCommandSource", - "AgentDriverContext", - "AgentDriverContextInput", - "AgentDriverContextPortOverrides", - "AgentDriverEventSink", - "AgentDriverFilePort", - "AgentDriverHostIntegrationPort", - "AgentDriverHostPorts", - "AgentDriverKernel", - "AgentDriverKernelCore", - "AgentDriverKernelOptions", - "AgentDriverKernelStartInput", - "AgentDriverMaterializedSkill", - "AgentDriverMcpPort", - "AgentDriverPermissionPort", - "AgentDriverProviderDescriptor", - "AgentDriverRuntimeEvent", - "AgentDriverSkillPort", - "CmaAgentRecord", - "CmaClaimInboundEventInput", - "CmaClaimInboundEventResult", - "CmaCreateAgentInput", - "CmaCreateEnvironmentInput", - "CmaCreateSessionInput", - "CmaEnvironmentConfig", - "CmaEnvironmentLimitedNetworking", - "CmaEnvironmentNetworking", - "CmaEnvironmentPackageManager", - "CmaEnvironmentPackages", - "CmaEnvironmentRecord", - "CmaEnvironmentUnrestrictedNetworking", - "CmaHttpAuthorizationContext", - "CmaHttpAuthorizer", - "CmaHttpBetaHeaderRequirement", - "CmaHttpDriverCommandDispatchInput", - "CmaHttpDriverCommandDispatcher", - "CmaHttpHandler", - "CmaHttpHandlerOptions", - "CmaInboundEvent", - "CmaInboundEventLease", - "CmaInvalidEventError", - "CmaMemoryStoreIdFactory", - "CmaMemoryStoreOptions", - "CmaOutboundEvent", - "CmaRenewInboundEventClaimInput", - "CmaSdkBetaHeader", - "CmaSdkClient", - "CmaSdkClientOptions", - "CmaSdkError", - "CmaSdkFetch", - "CmaSdkRequestOptions", - "CmaSdkStreamOptions", - "CmaSessionEventDispatchRecord", - "CmaSessionEventRecord", - "CmaSessionRecord", - "CmaSessionStatus", - "CmaSessionTerminatedError", - "CmaSettleInboundEventInput", - "CmaStore", - "CmaStoreConflictError", - "CmaStoreNotFoundError", - "CmaStoreResourceKind", - "CmaUnsupportedFieldError", - "CmaUserCustomToolResultEvent", - "CmaUserInterruptEvent", - "CmaUserMessageEvent", - "CmaUserToolConfirmationEvent", - "DriverCapability", - "DriverCapabilityId", - "DriverDiagnosticCode", - "DriverDiagnosticInput", - "DriverDiagnosticSeverity", - "DriverEvent", - "DriverEventEnvelope", - "DriverEventInput", - "DriverExecutionInput", - "DriverExecutionRunInput", - "DriverExecutionSessionInput", - "DriverHostIntegrationSnapshot", - "DriverId", - "DriverInstanceId", - "DriverNativeRuntimeRef", - "DriverNativeRuntimeRefKind", - "DriverPermissionRequest", - "DriverRuntime", - "DriverRuntimeTransport", - "DriverStartInput", - "EventId", - "InputStartCommand", - "InputStartCommandResult", - "McpExecuteCommand", - "McpExecuteCommandResult", - "MessageId", - "PermissionResolveCommand", - "RunError", - "RunId", - "RuntimeCommand", - "RuntimeCommandInput", - "RuntimeCommandResult", - "RuntimeCommandStatus", - "SemanticDriverId", - "SessionId", - "SessionStopCommand", - "TurnCancelCommand" - ], - "values": [ - "AGENT_DRIVER_PROVIDER_REGISTRY", - "AgentDriverKernelCore", - "CMA_DEFAULT_BETA_HEADER_NAME", - "CMA_DEFAULT_BETA_HEADER_VALUE", - "CmaInvalidEventError", - "CmaSdkClient", - "CmaSdkError", - "CmaSessionTerminatedError", - "CmaStoreConflictError", - "CmaStoreNotFoundError", - "CmaUnsupportedFieldError", - "DRIVER_ID_INPUT_PATTERN", - "DRIVER_ID_PATTERN", - "SUPPORTED_DRIVER_NATIVE_RUNTIME_REF_KINDS", - "SUPPORTED_DRIVER_RUNTIMES", - "SUPPORTED_DRIVER_RUNTIME_TRANSPORTS", - "createAgentDriverContext", - "createAgentDriverProviderCapabilities", - "createCmaHttpHandler", - "createCmaMemoryStore", - "createDriverDiagnosticEvent", - "createDriverId", - "getExpectedDriverNativeRuntimeRefKind", - "isDriverId", - "isSupportedDriverRuntime", - "isSupportedDriverRuntimeTransport", - "normalizeDriverId", - "parseCmaInboundEvent", - "parseDriverEventEnvelope", - "parseDriverId", - "parseDriverNativeRuntimeRef", - "parseRuntimeCommand", - "projectCmaInboundToDriverCommand", - "projectDriverEventToCma", - "pushDriverDiagnosticEvent" - ] - }, - "./boot": { - "types": [ - "AccountId", - "AgentDeploymentVersionId", - "AgentId", - "AuthorizedDriverBootMcpServer", - "CredentialId", - "DriverBootMcpServer", - "DriverBootPayload", - "DriverBuiltInToolConfig", - "DriverBuiltInToolName", - "DriverConfigRevision", - "DriverExecutionEnvironment", - "DriverExecutionSessionContext", - "DriverExecutionSessionSpec", - "DriverExecutionSpec", - "DriverNativeRuntimeRef", - "DriverNativeRuntimeRefKind", - "DriverOrigin", - "DriverPermissionPolicy", - "DriverRecoveryMessage", - "DriverResolvedSkill", - "DriverRuntime", - "DriverRuntimeTransport", - "DriverSkillCatalogEntry", - "DriverSkillCatalogFrontmatterSummary", - "EnvironmentId", - "EnvironmentRevisionId", - "McpServerId", - "SandboxId", - "SandboxSessionId", - "SkillId", - "SkillSnapshotId", - "UnavailableDriverBootMcpServer" - ], - "values": [ - "DEFAULT_DRIVER_PERMISSION_POLICY", - "DRIVER_BOOT_PAYLOAD_ENV_NAME", - "DRIVER_BOOT_PAYLOAD_FILE_ENV_NAME", - "DRIVER_CONTROL_PORT_MAX", - "DRIVER_CONTROL_PORT_MIN", - "DRIVER_PROTOCOL_VERSION", - "SUPPORTED_DRIVER_NATIVE_RUNTIME_REF_KINDS", - "SUPPORTED_DRIVER_RUNTIMES", - "SUPPORTED_DRIVER_RUNTIME_TRANSPORTS", - "isSupportedDriverRuntime", - "isSupportedDriverRuntimeTransport", - "parseDriverBootPayload", - "parseDriverBootPayloadJson" - ] - }, - "./cma-http": { - "types": [ - "CmaHttpAuthorizationContext", - "CmaHttpAuthorizer", - "CmaHttpBetaHeaderRequirement", - "CmaHttpDriverCommandDispatchInput", - "CmaHttpDriverCommandDispatcher", - "CmaHttpHandler", - "CmaHttpHandlerOptions" - ], - "values": [ - "CMA_DEFAULT_BETA_HEADER_NAME", - "CMA_DEFAULT_BETA_HEADER_VALUE", - "createCmaHttpHandler" - ] - }, - "./cma-sdk": { - "types": [ - "CmaSdkBetaHeader", - "CmaSdkClient", - "CmaSdkClientOptions", - "CmaSdkError", - "CmaSdkFetch", - "CmaSdkRequestOptions", - "CmaSdkStreamOptions", - "CmaSessionEventDispatchRecord" - ], - "values": ["CmaSdkClient", "CmaSdkError"] - }, - "./contract": { - "types": [ - "AppendPreview", - "ArtifactItem", - "Audience", - "AuthorityOperation", - "AuthorityOutcomeUnknownError", - "BlobManifestEntry", - "BlobRef", - "BlobRefContent", - "BlobReferenceKey", - "Capabilities", - "CapabilityName", - "ChangeItem", - "CleanupObligation", - "Command", - "CommandGetParams", - "CommandKind", - "CommandReceipt", - "CommandRecord", - "CommandStatus", - "CommittedMutation", - "ConfigOption", - "ContentBlock", - "ContractInvariantCode", - "ContractInvariantError", - "CoreCapability", - "CoreMethod", - "CoreResourceKind", - "ExecutorAttachParams", - "ExecutorAttachResult", - "ExecutorCommandParams", - "ExecutorCommandResultParams", - "ExecutorMutateParams", - "ExecutorPreviewSubmission", - "ExecutorRenewParams", - "ExtensionContent", - "ExtensionInteraction", - "ExtensionItem", - "Extensions", - "FileChange", - "Implementation", - "InitializeParams", - "InitializeResult", - "InlineBlobContent", - "InputInteraction", - "InputQuestion", - "InputResolution", - "Interaction", - "InteractionKind", - "InteractionResolution", - "Item", - "ItemKind", - "ItemStatus", - "JsonContent", - "JsonObject", - "JsonRpcError", - "JsonRpcId", - "JsonRpcNotification", - "JsonRpcRequest", - "JsonRpcSuccess", - "JsonValue", - "Lease", - "LeaseFence", - "MessageItem", - "MutationCause", - "MutationReceipt", - "MutationSync", - "PeerLimits", - "PermissionInteraction", - "PermissionOption", - "PermissionResolution", - "PlanEntry", - "PlanItem", - "PreviewApplyResult", - "PreviewBatch", - "PreviewBuffer", - "PreviewBufferOptions", - "PreviewStreamState", - "PreviewUpdate", - "ProposedMutation", - "ProtocolAdmissionLimits", - "ProtocolError", - "ProtocolLimits", - "Provenance", - "PutOperation", - "ReasoningItem", - "RemoveOperation", - "ReplacePreview", - "RequestDigest", - "ResourceKey", - "ResourceKind", - "ResourceLinkContent", - "Run", - "RunStatus", - "Session", - "SessionActivity", - "SessionSnapshot", - "SnapshotSync", - "SubscribeParams", - "SubscribeResult", - "SubscriptionUpdate", - "SyncAdmissionLimits", - "SyncPayload", - "TerminalItem", - "TextContent", - "TokenUsage", - "ToolInteraction", - "ToolItem", - "ToolResolution", - "UnsubscribeParams" - ], - "values": [ - "AuthorityOutcomeUnknownError", - "COMMAND_KINDS", - "CORE_CAPABILITIES", - "CORE_METHODS", - "CORE_PREVIEW_CHANNELS", - "ContractInvariantError", - "JSON_RPC_ERROR_CODES", - "PROTOCOL_VERSION", - "appendPreviewSchema", - "applyCommittedMutation", - "applyPreviewUpdate", - "applySyncPayload", - "artifactItemSchema", - "assertFrameAdmission", - "assertProtocolAdmission", - "audienceSchema", - "authorityContent", - "authorityOperationSchema", - "blobManifestEntrySchema", - "blobRefContentSchema", - "blobRefSchema", - "blobReferenceKeySchema", - "capabilitiesSchema", - "capabilityNameSchema", - "changeItemSchema", - "cleanupObligationSchema", - "coalescePreviewUpdates", - "commandGetParamsSchema", - "commandKindSchema", - "commandReceiptSchema", - "commandRecordSchema", - "commandSchema", - "commandStatusSchema", - "committedMutationSchema", - "compareTimestamps", - "configOptionSchema", - "contentBlockSchema", - "contentExtensionNames", - "coreCapabilitySchema", - "coreMethodSchema", - "corePreviewChannelSchema", - "coreResourceKindSchema", - "createPreviewBuffer", - "deriveSessionActivity", - "executorAttachParamsSchema", - "executorAttachResultSchema", - "executorCommandParamsSchema", - "executorCommandResultParamsSchema", - "executorMutateParamsSchema", - "executorPreviewSubmissionSchema", - "executorRenewParamsSchema", - "extensionContentSchema", - "extensionInteractionSchema", - "extensionItemSchema", - "extensionNameSchema", - "extensionsSchema", - "fileChangeSchema", - "hasBlobRef", - "implementationSchema", - "initializeParamsSchema", - "initializeResultSchema", - "inlineBlobContentSchema", - "inputInteractionSchema", - "inputQuestionSchema", - "inputResolutionSchema", - "interactionResolutionSchema", - "interactionSchema", - "itemSchema", - "itemStatusSchema", - "jsonByteLength", - "jsonContentSchema", - "jsonObjectSchema", - "jsonRpcErrorSchema", - "jsonRpcIdSchema", - "jsonRpcNotificationSchema", - "jsonRpcRequestSchema", - "jsonRpcSuccessSchema", - "jsonValueSchema", - "leaseFenceSchema", - "leaseSchema", - "messageItemSchema", - "methodSchemas", - "mutationCauseSchema", - "mutationReceiptSchema", - "mutationSyncSchema", - "normalizeExecutorMutation", - "opaqueIdSchema", - "peerLimitsSchema", - "permissionInteractionSchema", - "permissionOptionSchema", - "permissionResolutionSchema", - "planEntrySchema", - "planItemSchema", - "previewBatchSchema", - "previewChannelSchema", - "previewUpdateSchema", - "proposedMutationSchema", - "protocolErrorSchema", - "protocolIdSchema", - "protocolLimitsSchema", - "protocolVersionSchema", - "provenanceSchema", - "putOperationSchema", - "reasoningItemSchema", - "removeOperationSchema", - "replacePreviewSchema", - "requestDigestSchema", - "resourceKeySchema", - "resourceKindSchema", - "resourceLinkContentSchema", - "revisionSchema", - "runSchema", - "sessionSchema", - "sessionSnapshotSchema", - "sha256Schema", - "snapshotSyncSchema", - "subscribeParamsSchema", - "subscribeResultSchema", - "subscriptionUpdateSchema", - "syncPayloadSchema", - "terminalItemSchema", - "textContentSchema", - "timestampSchema", - "tokenUsageSchema", - "toolInteractionSchema", - "toolItemSchema", - "toolLocationSchema", - "toolResolutionSchema", - "unsubscribeParamsSchema", - "validateCommand", - "validateExecutorMutation", - "validatePreviewBatch", - "validateSessionSnapshot" - ] - }, - "./events": { - "types": ["DriverEvent", "DriverEventEnvelope", "DriverEventInput"], - "values": ["parseDriverEventEnvelope"] - }, - "./orpc": { - "types": [ - "DriverCommandUpdateInput", - "DriverCompletionInput", - "DriverEventBatchInput", - "DriverEventBatchOutput", - "DriverEventReceipt", - "DriverExternalToolEffectClaimInput", - "DriverExternalToolEffectClaimOutput", - "DriverExternalToolEffectCompleteInput", - "DriverExternalToolEffectUnknownInput", - "DriverFailureInput", - "DriverHeartbeatInput", - "DriverHeartbeatOutput", - "DriverHelloInput", - "DriverHelloOutput", - "DriverLogBatchInput", - "DriverLogBatchOutput", - "DriverLogContext", - "DriverLogEntry", - "DriverLogError", - "DriverNextCommandInput", - "DriverNextCommandOutput", - "DriverReadyInput", - "DriverRpcOptions", - "DriverRuntimeClient" - ], - "values": [ - "parseDriverCommandUpdateInput", - "parseDriverCompletionInput", - "parseDriverEventBatchInput", - "parseDriverFailureInput", - "parseDriverHeartbeatInput", - "parseDriverHelloInput", - "parseDriverLogBatchInput", - "parseDriverNextCommandInput", - "parseDriverReadyInput" - ] - }, - "./paths": { - "types": [], - "values": [ - "SANDBOX_CACHE_PATH", - "SANDBOX_MEMORY_PATH", - "SANDBOX_ORGANIZATION_ROOT", - "SANDBOX_SESSION_ROOT", - "SANDBOX_SESSION_STATE_DIR", - "SANDBOX_WORKSPACE_ROOT", - "getSessionOrganizationPath", - "getSessionResourceRootPath", - "getSessionRuntimeStatePath", - "getSessionStateRootPath", - "getSessionWorkspacePath", - "isSandboxCachePath", - "isSandboxMemoryPath", - "isSandboxOrganizationPath", - "isSandboxSessionPath", - "isSandboxSessionStatePath", - "normalizeSandboxFileBrowserPath" - ] - }, - "./provider-output": { - "types": ["OpenAiPrivateCitationFilterResult", "OpenAiPrivateCitationStreamFilter"], - "values": ["OpenAiPrivateCitationStreamFilter", "filterOpenAiPrivateCitations"] - }, - "./runtime": { - "types": [ - "DriverNativeRuntimeRef", - "DriverNativeRuntimeRefKind", - "DriverRuntime", - "DriverRuntimeTransport" - ], - "values": [ - "SUPPORTED_DRIVER_NATIVE_RUNTIME_REF_KINDS", - "SUPPORTED_DRIVER_RUNTIMES", - "SUPPORTED_DRIVER_RUNTIME_TRANSPORTS", - "getExpectedDriverNativeRuntimeRefKind", - "isSupportedDriverRuntime", - "isSupportedDriverRuntimeTransport", - "parseDriverNativeRuntimeRef" - ] - } -} diff --git a/tests/public-api-exports.test.ts b/tests/public-api-exports.test.ts deleted file mode 100644 index c5b2d2d..0000000 --- a/tests/public-api-exports.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { expect, test } from "bun:test"; -import { readFileSync, writeFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -import { API, SymbolFlags, type Project } from "typescript/unstable/async"; - -const repositoryRoot = fileURLToPath(new URL("../", import.meta.url)); -const snapshotPath = resolve(repositoryRoot, "tests/public-api-exports.snapshot.json"); - -interface PackageManifest { - readonly exports: Readonly>; -} - -interface EntryExports { - readonly types: readonly string[]; - readonly values: readonly string[]; -} - -type ExportSnapshot = Readonly>; - -async function collectEntryExports(project: Project, entryPath: string): Promise { - const source = await project.program.getSourceFile(entryPath); - - if (source === undefined) { - throw new Error(`Public package entry is missing: ${entryPath}.`); - } - - const values = new Set(Object.keys(await import(pathToFileURL(entryPath).href))); - const moduleSymbol = await project.checker.getSymbolAtLocation(source); - - if (moduleSymbol === undefined) { - throw new Error(`Public package entry has no module symbol: ${entryPath}.`); - } - - const types = new Set(); - - for (const exported of await project.checker.getExportsOfModule(moduleSymbol)) { - const target = - exported.flags & SymbolFlags.Alias - ? await project.checker.getAliasedSymbol(exported) - : exported; - - if (await project.checker.isUnknownSymbol(target)) { - throw new Error(`Public API export ${exported.name} is unresolved.`); - } - - if (target.flags & (SymbolFlags.Type | SymbolFlags.Namespace)) { - types.add(exported.name); - } - } - - return { types: [...types].toSorted(), values: [...values].toSorted() }; -} - -test("public package entries preserve their complete value and type export sets", async () => { - const configPath = resolve(repositoryRoot, "tsconfig.json"); - const api = new API({ cwd: repositoryRoot }); - - try { - const snapshot = await api.updateSnapshot({ openProjects: [configPath] }); - const project = snapshot.getProject(configPath); - - if (project === undefined) { - throw new Error(`TypeScript project is missing: ${configPath}.`); - } - - expect( - await collectEntryExports( - project, - resolve(repositoryRoot, "tests/fixtures/public-api-type-namespace.ts"), - ), - ).toEqual({ - types: [ - "AuditBoth", - "AuditDeclaredNamespace", - "AuditDefault", - "AuditMerged", - "AuditNamespace", - "AuditType", - "AuditTypeNamespace", - "Named", - "TypeStarBoth", - "TypeStarOnly", - "default", - ], - values: [ - "AuditBoth", - "AuditDefault", - "AuditMerged", - "AuditNamespace", - "AuditValue", - "default", - ], - }); - - const manifest = JSON.parse( - readFileSync(resolve(repositoryRoot, "package.json"), "utf8"), - ) as PackageManifest; - const current = Object.fromEntries( - ( - await Promise.all( - Object.entries(manifest.exports).map( - async ([entry, target]) => - [ - entry, - await collectEntryExports(project, resolve(repositoryRoot, target.default)), - ] as const, - ), - ) - ).toSorted(([left], [right]) => left.localeCompare(right)), - ); - - if (process.env["UPDATE_PUBLIC_EXPORT_SNAPSHOT"] === "1") { - writeFileSync(snapshotPath, `${JSON.stringify(current, null, 2)}\n`); - } - - const expected = JSON.parse(readFileSync(snapshotPath, "utf8")) as ExportSnapshot; - expect(current).toEqual(expected); - } finally { - await api.close(); - } -}); diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index 701f8a8..9cc8dd0 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -1,14 +1,14 @@ import { describe, expect, test } from "bun:test"; -import { DRIVER_PROTOCOL_VERSION as DRIVER_PROTOCOL_VERSION_FROM_BOOT_SUBPATH } from "../src/boot"; -import { createCmaHttpHandler as createCmaHttpHandlerFromSubpath } from "../src/cma-http"; -import { CmaSdkClient as CmaSdkClientFromSubpath } from "../src/cma-sdk"; +import { DRIVER_PROTOCOL_VERSION as DRIVER_PROTOCOL_VERSION_FROM_BOOT_SUBPATH } from "@mosoo/agent-driver/boot"; +import { createCmaHttpHandler as createCmaHttpHandlerFromSubpath } from "@mosoo/agent-driver/cma-http"; +import { CmaSdkClient as CmaSdkClientFromSubpath } from "@mosoo/agent-driver/cma-sdk"; import { PROTOCOL_VERSION as PROTOCOL_VERSION_FROM_CONTRACT, protocolVersionSchema as protocolVersionSchemaFromContract, sessionSnapshotSchema as sessionSnapshotSchemaFromContract, -} from "../src/contract"; -import { parseDriverEventEnvelope as parseDriverEventEnvelopeFromSubpath } from "../src/events"; +} from "@mosoo/agent-driver/contract"; +import { parseDriverEventEnvelope as parseDriverEventEnvelopeFromSubpath } from "@mosoo/agent-driver/events"; import { AGENT_DRIVER_PROVIDER_REGISTRY, AgentDriverKernelCore, @@ -25,15 +25,16 @@ import { pushDriverDiagnosticEvent, projectCmaInboundToDriverCommand, projectDriverEventToCma, -} from "../src/index"; +} from "@mosoo/agent-driver"; import { parseDriverHeartbeatInput as parseDriverHeartbeatInputFromOrpcSubpath, parseDriverHelloInput as parseDriverHelloInputFromOrpcSubpath, parseDriverReadyInput as parseDriverReadyInputFromOrpcSubpath, -} from "../src/orpc"; -import type { DriverHeartbeatInput as DriverHeartbeatInputFromOrpcSubpath } from "../src/orpc"; -import { SANDBOX_MEMORY_PATH as SANDBOX_MEMORY_PATH_FROM_PATHS_SUBPATH } from "../src/paths"; -import { isSupportedDriverRuntime as isSupportedDriverRuntimeFromSubpath } from "../src/runtime"; +} from "@mosoo/agent-driver/orpc"; +import type { DriverHeartbeatInput as DriverHeartbeatInputFromOrpcSubpath } from "@mosoo/agent-driver/orpc"; +import { SANDBOX_MEMORY_PATH as SANDBOX_MEMORY_PATH_FROM_PATHS_SUBPATH } from "@mosoo/agent-driver/paths"; +import { filterOpenAiPrivateCitations } from "@mosoo/agent-driver/provider-output"; +import { isSupportedDriverRuntime as isSupportedDriverRuntimeFromSubpath } from "@mosoo/agent-driver/runtime"; describe("public API", () => { test("imports without starting the driver process", () => { @@ -100,5 +101,9 @@ describe("public API", () => { }); expect(isSupportedDriverRuntimeFromSubpath("openai-runtime")).toBe(true); expect(SANDBOX_MEMORY_PATH_FROM_PATHS_SUBPATH).toBe("/workspace/memory"); + expect(filterOpenAiPrivateCitations("plain text")).toEqual({ + privateCitationCount: 0, + text: "plain text", + }); }); }); diff --git a/vite.config.ts b/vite.config.ts index 4ff33c3..4f4aede 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,67 @@ import { defineConfig } from "vite-plus"; +function restrictImports( + ...patterns: string[] +): ["error", { paths: string[]; patterns: { regex: string }[] }] { + return [ + "error", + { + paths: ["module", "node:module"], + patterns: patterns.map((regex) => ({ regex })), + }, + ]; +} + export default defineConfig({ + lint: { + plugins: ["import", "typescript", "unicorn"], + rules: { + "import/no-commonjs": ["error", { allowConditionalRequire: false }], + "import/no-cycle": ["error", { ignoreExternal: true, ignoreTypes: false }], + "import/no-dynamic-require": ["error", { esmodule: true }], + "no-restricted-imports": restrictImports(), + "typescript/no-require-imports": "error", + "unicorn/prefer-module": "error", + }, + overrides: [ + { + files: ["src/contract/**/*.ts"], + rules: { + "no-restricted-imports": restrictImports( + "(^|/)(core|infrastructure|runtimes|stores|surfaces)(/|$)", + ), + }, + }, + { + files: ["src/protocol/**/*.ts"], + rules: { + "no-restricted-imports": restrictImports( + "(^|/)(core|infrastructure|runtimes)(/|$)", + "(^|/)\\.\\./runtime-events(/|$)", + ), + }, + }, + { + files: ["src/core/**/*.ts"], + rules: { + "no-restricted-imports": restrictImports( + "(^|/)(infrastructure|stores|surfaces)(/|$)", + "(^|/)runtimes/(acp|claude|openai)(/|$)", + ), + }, + }, + { + files: [ + "src/runtimes/acp/**/*.ts", + "src/runtimes/claude/**/*.ts", + "src/runtimes/openai/**/*.ts", + ], + rules: { + "no-restricted-imports": restrictImports("(^|/)(infrastructure|stores|surfaces)(/|$)"), + }, + }, + ], + }, pack: { dts: { emitDtsOnly: true }, entry: ["src/*.ts", "src/contract/index.ts"], From ea4bb9ccb4c14489fa0fe7818c0ca5976cff2743 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Fri, 28 Aug 2026 21:09:00 +0000 Subject: [PATCH 06/20] fix(protocol): remove Node-only public boundary types --- src/protocol/boot/index.ts | 3 +-- src/protocol/events/runtime-events.ts | 6 ++++-- src/runtime-command/index.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/protocol/boot/index.ts b/src/protocol/boot/index.ts index 7b091f2..cda3246 100644 --- a/src/protocol/boot/index.ts +++ b/src/protocol/boot/index.ts @@ -1,4 +1,3 @@ -import { delimiter, isAbsolute } from "node:path"; import { z } from "zod"; import type { DriverInstanceId } from "../id"; @@ -85,7 +84,7 @@ function omitUndefinedProperties>(value: V } const absolutePathSchema = nonEmptyStringSchema.refine( - (path) => isAbsolute(path) && !path.includes("\0") && !path.includes(delimiter), + (path) => path.startsWith("/") && !path.includes("\0") && !path.includes(":"), "must be an absolute path without null bytes or path delimiters", ); diff --git a/src/protocol/events/runtime-events.ts b/src/protocol/events/runtime-events.ts index 7b1f234..26fa4d3 100644 --- a/src/protocol/events/runtime-events.ts +++ b/src/protocol/events/runtime-events.ts @@ -52,6 +52,7 @@ export * from "./runtime-event-types"; const runtimeEventKindSet = new Set(RUNTIME_EVENT_KINDS); const MAX_AGENT_TASKS_REPLACED_PAYLOAD_BYTES = 1_020 * 1_024; +const utf8Encoder = new TextEncoder(); const agentTasksReplacedPayloadSchema = z .object({ tasks: z @@ -61,7 +62,7 @@ const agentTasksReplacedPayloadSchema = z taskId: z .string() .min(1) - .refine((value) => Buffer.byteLength(value, "utf8") <= 256), + .refine((value) => utf8Encoder.encode(value).byteLength <= 256), taskType: z.string().min(1).max(4_096).optional(), title: z.string().min(1).max(4_096).optional(), }) @@ -85,7 +86,8 @@ const agentTasksReplacedPayloadSchema = z .strict() .superRefine((payload, context) => { if ( - Buffer.byteLength(JSON.stringify(payload), "utf8") > MAX_AGENT_TASKS_REPLACED_PAYLOAD_BYTES + utf8Encoder.encode(JSON.stringify(payload)).byteLength > + MAX_AGENT_TASKS_REPLACED_PAYLOAD_BYTES ) { context.addIssue({ code: "custom", diff --git a/src/runtime-command/index.ts b/src/runtime-command/index.ts index 05e2d3c..176cbe2 100644 --- a/src/runtime-command/index.ts +++ b/src/runtime-command/index.ts @@ -189,7 +189,7 @@ function readOptionalString(record: Record, field: string): str } function normalizeControlReason(reason: string): string { - const bytes = Buffer.byteLength(reason, "utf8"); + const bytes = new TextEncoder().encode(reason).byteLength; return bytes <= MAX_RUNTIME_CONTROL_REASON_BYTES ? reason : `Runtime command reason exceeded ${String(MAX_RUNTIME_CONTROL_REASON_BYTES)} UTF-8 bytes (received ${String(bytes)}).`; From 446f136ef1ba752d6f1b142e5bcee5e48273b965 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Fri, 28 Aug 2026 21:32:31 +0000 Subject: [PATCH 07/20] chore(deps): update Claude Agent SDK --- Containerfile | 2 +- bun.lock | 20 ++++++++++---------- package.json | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Containerfile b/Containerfile index f0a4fba..ad9dcb8 100644 --- a/Containerfile +++ b/Containerfile @@ -4,7 +4,7 @@ FROM docker.io/oven/bun:${BUN_VERSION}@sha256:5ff609364c049b54eb0ff560ec96319729 FROM docker.io/cloudflare/sandbox:0.12.8@sha256:822501de5f0c52a012c125c4e5e4c0080421a8e93ca4ce0ba3d247148021989f # Keep this pin in sync with downstream mosoo apps/api/package.json -> @cloudflare/sandbox. -ARG CLAUDE_AGENT_SDK_VERSION=0.3.247 +ARG CLAUDE_AGENT_SDK_VERSION=0.3.251 ARG BUN_VERSION ARG OPENAI_RUNTIME_VERSION=0.150.1 ARG OPENCODE_VERSION=1.18.23 diff --git a/bun.lock b/bun.lock index 59f6602..e66a034 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@mosoo/agent-driver", "dependencies": { "@agentclientprotocol/sdk": "1.4.0", - "@anthropic-ai/claude-agent-sdk": "0.3.247", + "@anthropic-ai/claude-agent-sdk": "0.3.251", "@anthropic-ai/sdk": "0.121.0", "@modelcontextprotocol/client": "^2.0.0", "@orpc/client": "^1.15.0", @@ -27,23 +27,23 @@ "packages": { "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.4.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-/eufudw+aFY1LKLolT6yFE6UMmYRl7fMJ/DEONSIyR6wI3slHWITBsANRGqXEY8FRzqUxwh7QEaGiZHcJPVThg=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.247", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.247", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.247", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.247", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.247", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.247", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.247", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.247", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.247" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-T9BDPloZxABkGREDRbWA7TVa9SLBMWI5GQXw39+XotQLCIX3McRic9OJdUnAdXa7sutwQ4nNrXwTSazN4IphjA=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.251", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.251", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.251", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.251", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.251" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-DqSi8mH2tQYRlVV0G+lJnQ/WbjJZ/a+8cJ3vPuYoqh8esIIvXHm1ZOXV1UPGsFYRnbBytEoiSGitguEXd+sQ+Q=="], - "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.247", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vvCMAXy2KmiGHbJuThv+W6a+49qjofMR6S71FM8FTbkAAKg62cYcEyzVMVM3pNpBM5PtkMe8lRCqKnT9NUo6ng=="], + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.251", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C23h+Dbddcc4gai95+lhm4/94am2IOq+bf3IdEDDS7EPqDQqajo2s5q1/ZSwq9rOc0RJLT7Ws72ZCzYJh67KSg=="], - "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.247", "", { "os": "darwin", "cpu": "x64" }, "sha512-742ZXrdAxHvw+vzMFJMMvfbH8mWWCBajQH27BT+3fQU3Frjqu9gXKvA63dvQgxwjqO1gYU9TSy/A6PKq7zujaQ=="], + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.251", "", { "os": "darwin", "cpu": "x64" }, "sha512-HrLnb3ggk+vMbymUvbosgPmxWp4W6Ot+0mNVjoBYDn5OZrTV3C3LRtbWf7jwcGyKMcg0xJf0AqiudQ2BRrlr8A=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.247", "", { "os": "linux", "cpu": "arm64" }, "sha512-5/3ZQ9vOwrLv6w0CFjdbaTxPFyabHNLAEojkeiYmKzqlZ/taXG58/kVKgjqgx2qQhjcHCzy9JXDw5kp3RKJoGw=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.251", "", { "os": "linux", "cpu": "arm64" }, "sha512-3E27F5j82EWOyEniRW7crmo0jWYYQmPDdP52jWq6Y6aytiyYIdUFMEGZZ6chVHNlZiU7GsjTa3teKk2xlQ68lQ=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.247", "", { "os": "linux", "cpu": "arm64" }, "sha512-8UNRQcSy9lpLhknXryid5cTfxHxBZft4oK5R0XIIZUXY/tXvsNP3+kVYAHQUNyc1J8IDa15sd19iKkMJMqFodA=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.251", "", { "os": "linux", "cpu": "arm64" }, "sha512-RTUf7TPBUkQ6oV3pDkEdcD47I0uH0ZpvmZlXHnrLGznFqyLr+7XHupOxUMJD0Jwm9v5qeqpNiPAYB0dL4RYiHg=="], - "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.247", "", { "os": "linux", "cpu": "x64" }, "sha512-+tEIHgblLvBF+datSZb9WwnODXsFI7JxSFvm++oliH0eQO5zGDq0LD0hbY6h/A3nqODnBbIwVVBykwXMn68GPg=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.251", "", { "os": "linux", "cpu": "x64" }, "sha512-qCD87XPjcNM1u4ukqmcgpHDl3Y6l+cW8j7kPzH91Y5yLBYJ5xYZA2KtJ7AYNEPOteVyOGJw/efmva0S8CRr0fg=="], - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.247", "", { "os": "linux", "cpu": "x64" }, "sha512-iMLjkVHbcxQ5WY2WwieOJK57kD4c8Cc2tpHwpEmdTDsnWLfmNt3XFuJdmeROwyra+tcVA52oHL0dWr51+V50Pg=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.251", "", { "os": "linux", "cpu": "x64" }, "sha512-SzXrdy7jjrjJIuTG+VMRAY0YZ3G/8w21WuhAozwnvnEUAPo6NDv6lgFinu7NO8J6CPE+MK2sGze6JeCF+ljgUg=="], - "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.247", "", { "os": "win32", "cpu": "arm64" }, "sha512-Sni6U2xR55bUEJRv7Eikm6oC27IOn3rlVJZAzXRFCAgkf9+aBqq/dEUJiIcRT2xbXOIe5QdPGUmpnYfU1BsHMw=="], + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.251", "", { "os": "win32", "cpu": "arm64" }, "sha512-FH1ZLcyc97ie3h7CSlIxA1ppXILgf0V8sFQrWnHyKBrthJXQkMHXhsTq9OZV/2KhXslNf2hTe0gHpZ1nCL1Gmg=="], - "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.247", "", { "os": "win32", "cpu": "x64" }, "sha512-yWfZo2ER3EIS/3pv6EX1M/VrZXu+zFzKhgWJjGgFSlcupbaCAxrVBRTdCyfv5UYBKWH9kl5HjgV2HhLHq67LSw=="], + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.251", "", { "os": "win32", "cpu": "x64" }, "sha512-/jmtFIvfF0UFMxSZ8WV2Aus8aGA3+8Ft6AHvWuBJkY5jM2ubfqi6GnBjKCAL831TTllp2rKZlViANtWWRBvpvg=="], "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.121.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WFzwcH8l49CZHv0vQ9QQT51VJ1/DLDQfNSs9awpGlnKBdaSnqtdZa+tw/3cxxosTPIgK8uw1GqsJ2dSbCBD1Lg=="], diff --git a/package.json b/package.json index f0270ea..965f746 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "1.4.0", - "@anthropic-ai/claude-agent-sdk": "0.3.247", + "@anthropic-ai/claude-agent-sdk": "0.3.251", "@anthropic-ai/sdk": "0.121.0", "@modelcontextprotocol/client": "^2.0.0", "@orpc/client": "^1.15.0", From 90c3565571c7d21aa73c303c6037bc4cc8ff0745 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Sun, 30 Aug 2026 09:41:01 +0000 Subject: [PATCH 08/20] refactor(runtime)!: enforce durable SDK protocol boundaries Upgrade ACP 1.4, Codex 0.151, Claude Agent SDK 0.3.251, Anthropic 0.122, and OpenCode 1.18.25. Rebuild run terminals, permission delivery, MCP effect settlement, provider event projection, and generated Codex contracts around exact durable identities and bounded schema-first wire data. BREAKING CHANGE: Driver boot and RPC traffic now require protocol v3; control terminals require the exact runId and runtime event payloads use the v3 schemas. --- .github/workflows/pr.yml | 6 + Containerfile | 6 +- bun.lock | 52 +- package.json | 12 +- scripts/prune-openai-generated-types.mjs | 100 -- scripts/sync-openai-generated.mjs | 213 +++ src/bin/driver-process.ts | 54 +- src/core/agent-driver-backend.ts | 10 +- src/core/agent-driver-kernel.ts | 435 +++++-- src/core/driver-command-delivery.ts | 136 +- src/core/driver-command-dispatcher.ts | 882 +++++++++---- src/core/driver-permission-broker.ts | 268 +++- src/core/driver-runtime-io.ts | 88 +- src/core/driver-terminal-state.ts | 356 +++++ src/core/external-tool-effect-settlement.ts | 75 ++ src/host-ports/index.ts | 51 +- src/index.ts | 7 +- .../runtime/driver-instance-socket.ts | 433 ++++--- src/observability/driver-debug-events.ts | 8 +- src/observability/driver-debug-runtime.ts | 10 +- src/projections/cma/inbound.ts | 8 +- src/projections/cma/index.ts | 1 + src/protocol/boot/index.ts | 32 +- src/protocol/events/index.ts | 8 + src/protocol/events/runtime-event-types.ts | 19 +- .../events/runtime-event-validation.ts | 55 +- src/protocol/events/runtime-events.ts | 144 ++- src/protocol/orpc/index.ts | 294 +++-- src/runtime-command/index.ts | 218 +++- .../acp/acp-assistant-transcript-state.ts | 5 +- .../acp/acp-client-request-handler.ts | 97 +- src/runtimes/acp/acp-driver-backend.ts | 94 +- src/runtimes/acp/acp-file-system.ts | 26 +- src/runtimes/acp/acp-permission-events.ts | 2 +- src/runtimes/acp/acp-terminal-manager.ts | 250 +++- src/runtimes/acp/acp-turn-controller.ts | 6 + src/runtimes/claude/agent-sdk-event-writer.ts | 8 +- .../claude/agent-sdk-message-translator.ts | 13 +- src/runtimes/claude/agent-sdk-task-events.ts | 9 +- src/runtimes/driver-event-publisher.ts | 173 ++- src/runtimes/mcp/remote-http-mcp-executor.ts | 230 +++- .../openai/app-server-agent-task-events.ts | 4 +- .../openai/app-server-driver-backend.ts | 2 + .../openai/app-server-event-bridge.ts | 15 +- .../openai/app-server-event-mapping.ts | 56 + src/runtimes/openai/app-server-item-events.ts | 41 +- .../openai/generated-json-schema/README.md | 19 +- .../ServerNotification.json | 158 +++ .../ThreadResumeResponse.json | 158 +++ .../ThreadStartResponse.json | 158 +++ .../TurnStartResponse.json | 158 +++ .../openai/generated/ProtocolMethods.ts | 2 +- src/runtimes/openai/generated/README.md | 16 +- .../openai/generated/v2/CyberAccessProgram.ts | 9 + .../openai/generated/v2/ThreadResumeParams.ts | 4 +- .../openai/generated/v2/TurnStartParams.ts | 19 + .../openai/generated/v2/TurnToolOutput.ts | 10 + src/stores/cma-store.ts | 13 +- src/surfaces/cma-http/contract.ts | 6 +- src/surfaces/cma-http/request.ts | 2 + src/surfaces/cma-sdk/types.ts | 5 +- src/utils/async.ts | 2 +- tests/acp-client-request-handler.test.ts | 169 ++- tests/acp-driver-backend.test.ts | 479 ++++++- tests/acp-event-translator-lifecycle.test.ts | 19 + ...cp-event-translator-session-update.test.ts | 4 +- tests/acp-file-system.test.ts | 66 +- tests/acp-terminal-manager.test.ts | 461 ++++++- tests/agent-driver-kernel-async-queue.test.ts | 67 +- tests/agent-driver-kernel-commands.test.ts | 214 ++- tests/agent-driver-kernel-lifecycle.test.ts | 178 ++- tests/async.test.ts | 24 + tests/claude-agent-sdk-driver-backend.test.ts | 21 +- tests/claude-agent-sdk-durability.test.ts | 2 +- ...ent-sdk-provider-fixtures-terminal.test.ts | 12 +- ...t-sdk-provider-fixtures-transcript.test.ts | 55 + tests/claude-agent-sdk-task-events.test.ts | 39 +- tests/cma-http-dispatch.test.ts | 2 +- tests/cma-http-request.test.ts | 16 + tests/cma-http-sse.test.ts | 2 +- tests/cma-projection.test.ts | 14 +- tests/cma-sdk.test.ts | 8 +- tests/cma-store-claims.test.ts | 7 +- tests/cma-store-events.test.ts | 11 +- tests/cma-store-stream.test.ts | 7 +- tests/driver-artifact-live.test.ts | 27 +- tests/driver-artifact-mcp.test.ts | 91 +- tests/driver-artifact-test-controller.test.ts | 64 +- tests/driver-artifact-test-controller.ts | 194 ++- tests/driver-boot-payload-fixture.ts | 4 +- tests/driver-boot-schema.test.ts | 13 + .../driver-event-publisher-admission.test.ts | 234 +++- ...river-event-publisher-backpressure.test.ts | 4 +- tests/driver-event-publisher-drain.test.ts | 50 +- tests/driver-external-tool-effect-v3.test.ts | 1149 +++++++++++++++++ tests/driver-golden-fixtures.test.ts | 448 ++++++- tests/driver-instance-socket-process.test.ts | 130 +- tests/driver-instance-socket-socket.test.ts | 319 +++-- tests/driver-permission-broker.test.ts | 73 +- tests/driver-permission-policy.test.ts | 2 +- tests/driver-rpc-wire-v3.test.ts | 629 +++++++++ ...driver-runtime-boundary-dispatcher.test.ts | 963 +++++++++++++- ...er-runtime-boundary-event-envelope.test.ts | 86 +- tests/driver-runtime-boundary-fixtures.ts | 197 ++- tests/driver-runtime-boundary-state.test.ts | 873 +------------ tests/driver-runtime-boundary-timing.test.ts | 243 ++-- tests/driver-terminal-state.test.ts | 222 ++++ tests/external-tool-effect-settlement.test.ts | 158 +++ .../fixtures/driver/commands/input-start.json | 2 +- .../fixtures/driver/commands/mcp-execute.json | 1 + .../driver/commands/permission-resolve.json | 3 +- .../fixtures/driver/commands/turn-cancel.json | 3 +- .../tool-call-deltas.json | 13 + .../agent-tasks-replaced.json | 2 +- .../diagnostic-reported.json | 2 +- .../message-delta.json | 2 +- .../permission-requested.json | 2 +- .../runtime-event-envelopes/run-started.json | 2 +- .../tool-call-deltas.json | 24 + .../tool-call-updated.json | 2 +- .../usage-updated.json | 2 +- .../providers/acp/cases/max-turn-failure.json | 3 +- .../acp/cases/thought-and-unknown-update.json | 3 +- .../stream-text-thinking-tool-result.json | 17 +- .../cases/command-output-stream.json | 4 +- tests/openai-app-server-client.test.ts | 2 +- ...enai-app-server-event-bridge-items.test.ts | 13 +- ...-server-event-bridge-run-lifecycle.test.ts | 27 +- ...i-app-server-event-bridge-terminal.test.ts | 41 +- ...app-server-event-bridge-transcript.test.ts | 66 + ...penai-app-server-provider-fixtures.test.ts | 48 +- tests/openai-app-server-startup.test.ts | 21 +- tests/public-api.test.ts | 14 +- tests/remote-http-mcp-executor.test.ts | 357 ++++- 134 files changed, 11625 insertions(+), 2852 deletions(-) delete mode 100644 scripts/prune-openai-generated-types.mjs create mode 100644 scripts/sync-openai-generated.mjs create mode 100644 src/core/driver-terminal-state.ts create mode 100644 src/core/external-tool-effect-settlement.ts create mode 100644 src/runtimes/openai/generated/v2/CyberAccessProgram.ts create mode 100644 src/runtimes/openai/generated/v2/TurnToolOutput.ts create mode 100644 tests/driver-external-tool-effect-v3.test.ts create mode 100644 tests/driver-rpc-wire-v3.test.ts create mode 100644 tests/driver-terminal-state.test.ts create mode 100644 tests/external-tool-effect-settlement.test.ts create mode 100644 tests/fixtures/driver/runtime-event-drafts/tool-call-deltas.json create mode 100644 tests/fixtures/driver/runtime-event-envelopes/tool-call-deltas.json diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 23fb47a..1a9ac62 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -61,3 +61,9 @@ jobs: - name: Check run: vp run check + + - name: Build image + run: buildah build --http-proxy=false --platform linux/amd64 -t agent-driver:local . + + - name: Test image environment + run: vp run test:image:environment diff --git a/Containerfile b/Containerfile index ad9dcb8..fe3e3fc 100644 --- a/Containerfile +++ b/Containerfile @@ -1,13 +1,13 @@ ARG BUN_VERSION=1.4.0 FROM docker.io/oven/bun:${BUN_VERSION}@sha256:5ff609364c049b54eb0ff560ec96319729a972078ef2c755d758f0c6ef89c2d6 AS bun-runtime -FROM docker.io/cloudflare/sandbox:0.12.8@sha256:822501de5f0c52a012c125c4e5e4c0080421a8e93ca4ce0ba3d247148021989f +FROM docker.io/cloudflare/sandbox:0.12.9@sha256:4a56a37a3cfd9b38d65bb4b5d0b341e6490a3a4c0226274ae4c1cca4948e85fe # Keep this pin in sync with downstream mosoo apps/api/package.json -> @cloudflare/sandbox. ARG CLAUDE_AGENT_SDK_VERSION=0.3.251 ARG BUN_VERSION -ARG OPENAI_RUNTIME_VERSION=0.150.1 -ARG OPENCODE_VERSION=1.18.23 +ARG OPENAI_RUNTIME_VERSION=0.151.0 +ARG OPENCODE_VERSION=1.18.25 COPY --from=bun-runtime /usr/local/bin/bun /usr/local/bin/bun RUN test "$(bun --version)" = "$BUN_VERSION" diff --git a/bun.lock b/bun.lock index e66a034..63be142 100644 --- a/bun.lock +++ b/bun.lock @@ -7,18 +7,18 @@ "dependencies": { "@agentclientprotocol/sdk": "1.4.0", "@anthropic-ai/claude-agent-sdk": "0.3.251", - "@anthropic-ai/sdk": "0.121.0", + "@anthropic-ai/sdk": "0.122.0", "@modelcontextprotocol/client": "^2.0.0", "@orpc/client": "^1.15.0", "fflate": "^0.8.3", "vestig": "^0.24.1", - "zod": "^4.4.3", + "zod": "4.5.4", }, "devDependencies": { - "@openai/codex": "0.150.1", + "@openai/codex": "0.151.0", "@types/bun": "1.4.0", "@types/node": "^26.3.0", - "opencode-ai": "1.18.23", + "opencode-ai": "1.18.25", "typescript": "^7.0.2", "vite-plus": "0.3.0", }, @@ -45,7 +45,7 @@ "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.251", "", { "os": "win32", "cpu": "x64" }, "sha512-/jmtFIvfF0UFMxSZ8WV2Aus8aGA3+8Ft6AHvWuBJkY5jM2ubfqi6GnBjKCAL831TTllp2rKZlViANtWWRBvpvg=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.121.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WFzwcH8l49CZHv0vQ9QQT51VJ1/DLDQfNSs9awpGlnKBdaSnqtdZa+tw/3cxxosTPIgK8uw1GqsJ2dSbCBD1Lg=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.122.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-GGPNftt0caaz9MDlmNQGHX8855Ojaduyy5pm9Sm1h7HalCn0cWNb5/bweadJF+4yzbal+QL6ztBa09WAAOzLmQ=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], @@ -65,19 +65,19 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], - "@openai/codex": ["@openai/codex@0.150.1", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.150.1-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.150.1-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.150.1-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.150.1-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.150.1-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.150.1-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-knrbhpJH3mEULAVStcZW4F5WEt9MQhBj6KFOonBSIUGTLcHlu9CE7FRmr95E33y94+sWNZSeVBBV/kYvlfgxkQ=="], + "@openai/codex": ["@openai/codex@0.151.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.151.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.151.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.151.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.151.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.151.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.151.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-mhtWmOZRdmWD1jPbLDnQb59BsaVP/V+lXe/OFNR9ZcLZU0UCiBwn98Fcav1ss7sDIlHkuqj6nWd44IPeXoOhJA=="], - "@openai/codex-darwin-arm64": ["@openai/codex@0.150.1-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z614kKSI3/p+YMotBJMxCc4kvRCYEgsVmnaCG6U8HDraqTFxYcQYQ79B4/GBCBS88/KtacHI6qzjA+4Mu5BynA=="], + "@openai/codex-darwin-arm64": ["@openai/codex@0.151.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-g7YzpaCZGCw19R/gly3vRPjnLqaW7JcBAu2WQQ6e8PIlvBPmS/gMplIUURMgNO6gi8LsPzdlQtLqkwoeOOlIdg=="], - "@openai/codex-darwin-x64": ["@openai/codex@0.150.1-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-vcvJ7Q4IP2vA5ZR3v9Pj9VLKtlhD7efujxHqqW/NJ8F2/XqTD4iPjErV07190Om3wONW48yploTgul16QUk9Mg=="], + "@openai/codex-darwin-x64": ["@openai/codex@0.151.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-0y+g8TVpP+Fn10mjoKYXER6qYjn29w7xBUsbPXJ6Accu/FoM4Qp4WbKXQPmE0G0yUACTQVZRjzTSsdWUezNgkg=="], - "@openai/codex-linux-arm64": ["@openai/codex@0.150.1-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-U0BY5UDsCy1up/O2YzGtVtDbcumNerVyZVVx55PW9gGQsFOL0utOyBJiG/UiocFAXix8JNvNJxYMRt2Jx0FJKg=="], + "@openai/codex-linux-arm64": ["@openai/codex@0.151.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-CsLgFeX4TQ6I2Gdrxd2r5UbgIbDLCdtcLAlnMYjr06bCL057MTNGec7Ewb3+Z2DBiMuXCljdTBGqLOePkMV0sQ=="], - "@openai/codex-linux-x64": ["@openai/codex@0.150.1-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-B3Bu5MF/G9qFhbSMrdyZz9ocknXOo45DnOdWrREJaxWTjiuUO1ogoDR0/ttdc29JRFkbbXs3aC+Td8vIx3X0oA=="], + "@openai/codex-linux-x64": ["@openai/codex@0.151.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-xcVyY1FtwvVYhh2JBmz8fX8CQqFAxO/lxJ2IXsh8x5uwxZVHVl5fZHFHf8JdRaOGG0vpkYmu/DKKVoLd56/DDQ=="], - "@openai/codex-win32-arm64": ["@openai/codex@0.150.1-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-8I7N3ppKS1ql9GUr2RHS5Gw+KkRCKfIMIDDK/brxq6HizLgHoDK4CIR0OTvEgdX3Yh/2reBbxr9P4L1e+61e+Q=="], + "@openai/codex-win32-arm64": ["@openai/codex@0.151.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-zDWzOoh9wHm+Om1Nhn7os47rAVeSGPh0SnM3YOttdq6iPJz2zn4vBnbGUZjeih1qW/3mvNF3Oyd4owlaHmphmg=="], - "@openai/codex-win32-x64": ["@openai/codex@0.150.1-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-1P1tscFnw4A4ip/WN1R5WYPdfuAlxLctPpcE1QAPQnOtbkJR5NX8da4kNTQmiDN/Flx3TLJPDTRHP9Jn435eJg=="], + "@openai/codex-win32-x64": ["@openai/codex@0.151.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-sLT7xvID3jhU6tkzcwRPnMEclKRwUPbpo0mtfxIF9KpdZH3VJV7sM2/kXWXyvUM7Zt/YeyOaeATTEysbRz8Yog=="], "@orpc/client": ["@orpc/client@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0", "@orpc/standard-server-fetch": "1.15.0", "@orpc/standard-server-peer": "1.15.0" } }, "sha512-Qt0FdPSGySdQwy83iUWOq+Iqhw2gM9qHtyxfBbcw1mwOz1s4O2BwUFA3ymVTLIRRNYRgPrQLaBZhp8CacqeePg=="], @@ -551,31 +551,31 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "opencode-ai": ["opencode-ai@1.18.23", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.18.23", "opencode-darwin-x64": "1.18.23", "opencode-darwin-x64-baseline": "1.18.23", "opencode-linux-arm64": "1.18.23", "opencode-linux-arm64-musl": "1.18.23", "opencode-linux-x64": "1.18.23", "opencode-linux-x64-baseline": "1.18.23", "opencode-linux-x64-baseline-musl": "1.18.23", "opencode-linux-x64-musl": "1.18.23", "opencode-windows-arm64": "1.18.23", "opencode-windows-x64": "1.18.23", "opencode-windows-x64-baseline": "1.18.23" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-3NkT0XINL7d0HYkTyGV1SPChHXhvRgKqNaTgKRTGb0TXUWszXA7MW/y3zMZw29y1AQuUDAzRvVYmQ9KGRQhroA=="], + "opencode-ai": ["opencode-ai@1.18.25", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.18.25", "opencode-darwin-x64": "1.18.25", "opencode-darwin-x64-baseline": "1.18.25", "opencode-linux-arm64": "1.18.25", "opencode-linux-arm64-musl": "1.18.25", "opencode-linux-x64": "1.18.25", "opencode-linux-x64-baseline": "1.18.25", "opencode-linux-x64-baseline-musl": "1.18.25", "opencode-linux-x64-musl": "1.18.25", "opencode-windows-arm64": "1.18.25", "opencode-windows-x64": "1.18.25", "opencode-windows-x64-baseline": "1.18.25" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-pS4RKJ9eKwU7Dp5G5pdj1rhMnpG5APixXzfTKNoFqv9aFVI36Rnza2jESvKifxyPZlsA65MQB03WCArY0EK6mg=="], - "opencode-darwin-arm64": ["opencode-darwin-arm64@1.18.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QP9PjwpHtZoLVXw2WvUmPZecz7mWbQkT4t3K36B//fCaDG+zWa+SsztIeaW5azujNwwtUemLA5icE/zINng48Q=="], + "opencode-darwin-arm64": ["opencode-darwin-arm64@1.18.25", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W4dyMFtHBglWZ1SEooh3Ke9v1M9lv945Y58atb8e1yKII8YykJ8LknOFyKipYC028oPDO4IZc3GYGKbg9PCg2w=="], - "opencode-darwin-x64": ["opencode-darwin-x64@1.18.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-R9nWP3edz/0FnEfwmuxtiWBB7bS4NtZCyCffJyiMlrbwdDC+bIXYrWxWXVrzaP1mJujs6g2MAwTCUSx/qpBhDw=="], + "opencode-darwin-x64": ["opencode-darwin-x64@1.18.25", "", { "os": "darwin", "cpu": "x64" }, "sha512-YYKrfeUSJhD7hZl+yNmayS51sDwxiE9o5XwrfgYSSie6sOyHFc9Ei13VBkVU6T+IJHhFhTahOFAwSDxggrAnGA=="], - "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.18.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-QGx6I/nFYur7qJ/Nx2L3fC4XYQt44cyDsm7p8twNA+cdjGX3ndnPbMdAl5ikdZyAfSMUGYK8VWY2JMxv0rmfjw=="], + "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.18.25", "", { "os": "darwin", "cpu": "x64" }, "sha512-rRgTaoTeIN2diL1e1HGZ48Zh4ynMDEB1jYjD76LaFUVzMwakEY1i7NvG8e/rbjRMDkgXIr6TwzCtIMcMOpLQMA=="], - "opencode-linux-arm64": ["opencode-linux-arm64@1.18.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-g1zDFhuE9FOYwjSGderlu69wfd4GQzS0xsDiIY11QUciuBmM6DrHqLvmhlLFsUVHYVnCPY1YeN1pq5ewE3x72Q=="], + "opencode-linux-arm64": ["opencode-linux-arm64@1.18.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-PMvcpFpha3yAhaVCC0QbegHPxsEZ0FuQf+52PXvqQut1r3w1l1Pilor9tUA7TyCRa4UokACI90nTmKmtMnQBag=="], - "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.18.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-VyDkzUJfJgkx9h9RhazTW9xeTgSXBmVFPblsbPGBW9tR612f6gjQxfOfu4cpHnQHK1qjuW9ClzLKHWqv3EcJTA=="], + "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.18.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-IwIPKmNwIjLshlSgjoRLKFwxxiLpZ5Y0zjv6r456RQtJKK62IbYGXkCm3AiSZ/lqGsu3XF+xn/Xza29ivgpgcg=="], - "opencode-linux-x64": ["opencode-linux-x64@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-5x9d1Cm/YtqzR6lAlNbgVprTQ3R3hx7qGTWCzm5l5u6lBNkhYTrhy2s8k25dxKKuxqZ9Kngqz9JYWvSVHy2Lmw=="], + "opencode-linux-x64": ["opencode-linux-x64@1.18.25", "", { "os": "linux", "cpu": "x64" }, "sha512-bdRSJ6gbK/EnLNWxROOQYXFXiUeqeFxGz8DIO8LCqnii99A2OWFAyZ3Da5gpvfT1Yrp9/lYL55n/tM3ale5smg=="], - "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-yUhBOXfTQour2JCdAkwD3DDqSnyxB0grefwdPqEhYmJHIkYxfJIIzyy6V//pyouvkE0XMouFtiuZXw8S6Wo0iQ=="], + "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.18.25", "", { "os": "linux", "cpu": "x64" }, "sha512-+b0w7XyHx0XPQWHBk2JymXbXnyZQ2PjIPuu4a4QJgSUqGuGz1L2flA3wgpZVAWFUhrEIr9DFhBk3AkKKNgMuRw=="], - "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-c1DPxauhzAurlIBhJBr/rokDpc65l084T4qTl36gDDT9Xzc/Nk5Q5yMDaPm1DDI3WeHKDt11MDlxT5AjQW5gtw=="], + "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.18.25", "", { "os": "linux", "cpu": "x64" }, "sha512-E2JUeOOSXPbG1cNOzxnqjqkd0a3+oFmwkbJe6bZ308CFgLWBFfVh0fF42HTCEqfK+yYbidpEkQuEkUgxq/11IA=="], - "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.18.23", "", { "os": "linux", "cpu": "x64" }, "sha512-t/5mlnTBZKdZpqKHwdwxlWqGakntauvMSmXtyJc17M7XJRmZaaGHtNSaSefbbYFIL4agoQCXTvIkhhyxOvr7zQ=="], + "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.18.25", "", { "os": "linux", "cpu": "x64" }, "sha512-W15qTNDz1fsTzs1SkE6bB/gpIDBF3rwDbewUKdbyXD3dVs6umyugOql1T4u9n/gqWa/Z/VDURbn39VsejeSdbQ=="], - "opencode-windows-arm64": ["opencode-windows-arm64@1.18.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-QtJQcLU0yPz6on3jjks3f/EHgZuIDFw7FvAKu3wsHhL09NYDh7GczfRXDPRHa3NgqnU8dkB8p9mhqgaPRPogoQ=="], + "opencode-windows-arm64": ["opencode-windows-arm64@1.18.25", "", { "os": "win32", "cpu": "arm64" }, "sha512-GFp74pProoPwqktHMf+9wQ8fza1RvFt0RG0iRtTQnJ4VWVY62qEeVuJkH6ki9QXS270HXyqtxvF8AuHQzuVZlA=="], - "opencode-windows-x64": ["opencode-windows-x64@1.18.23", "", { "os": "win32", "cpu": "x64" }, "sha512-mMaIITuXzkNfjdcYL8uZaZuMDjulFyH/UCq9bxblam2mUZf9uWisoi5J6CXFsS/mkN7CZfTAt6PttSp4n3PH4g=="], + "opencode-windows-x64": ["opencode-windows-x64@1.18.25", "", { "os": "win32", "cpu": "x64" }, "sha512-xW5wtSxWYbI7DcmQWMlNWIiDBdMJON1vDiEmVWo88R9tT/PaahOhWKgp7FoWDqJKf89jS3ZIzkqnkU3F2dio7A=="], - "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.18.23", "", { "os": "win32", "cpu": "x64" }, "sha512-AqXsTKaPcDx3rrid5bLUwJbQ/3vr9rJ6fvOStIznTzwrbOgP8wy5G4jCoIzu6KB/WxGx/d1MrV4cGaJ73qnjBA=="], + "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.18.25", "", { "os": "win32", "cpu": "x64" }, "sha512-/28bGRQwT+2JdGbtGaNr95tstgysiULEXtcvgNg7yLDxitqmSVgd8V8XRGS0UWDdfiWWMND9A9T5EAsbF1/xDQ=="], "oxfmt": ["oxfmt@0.64.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.64.0", "@oxfmt/binding-android-arm64": "0.64.0", "@oxfmt/binding-darwin-arm64": "0.64.0", "@oxfmt/binding-darwin-x64": "0.64.0", "@oxfmt/binding-freebsd-x64": "0.64.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", "@oxfmt/binding-linux-arm64-gnu": "0.64.0", "@oxfmt/binding-linux-arm64-musl": "0.64.0", "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", "@oxfmt/binding-linux-riscv64-musl": "0.64.0", "@oxfmt/binding-linux-s390x-gnu": "0.64.0", "@oxfmt/binding-linux-x64-gnu": "0.64.0", "@oxfmt/binding-linux-x64-musl": "0.64.0", "@oxfmt/binding-openharmony-arm64": "0.64.0", "@oxfmt/binding-win32-arm64-msvc": "0.64.0", "@oxfmt/binding-win32-ia32-msvc": "0.64.0", "@oxfmt/binding-win32-x64-msvc": "0.64.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg=="], @@ -705,7 +705,7 @@ "yuku-parser": ["yuku-parser@0.5.48", "", { "dependencies": { "@yuku-toolchain/types": "0.5.43" }, "optionalDependencies": { "@yuku-parser/binding-darwin-arm64": "0.5.48", "@yuku-parser/binding-darwin-x64": "0.5.48", "@yuku-parser/binding-freebsd-x64": "0.5.48", "@yuku-parser/binding-linux-arm-gnu": "0.5.48", "@yuku-parser/binding-linux-arm-musl": "0.5.48", "@yuku-parser/binding-linux-arm64-gnu": "0.5.48", "@yuku-parser/binding-linux-arm64-musl": "0.5.48", "@yuku-parser/binding-linux-x64-gnu": "0.5.48", "@yuku-parser/binding-linux-x64-musl": "0.5.48", "@yuku-parser/binding-win32-arm64": "0.5.48", "@yuku-parser/binding-win32-x64": "0.5.48" } }, "sha512-OWBfhrpgK9+/4+IXG9oT8Bao4AhViQA7vdyNNH7EUg8dQYgwa70XtIBWTpCEme1P1ECyoDNYkn0wT63f8XRcVA=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], diff --git a/package.json b/package.json index 965f746..0692fc0 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "tc": "vp exec tsc", "test": "bun test", "test:coverage": "bun test --coverage", + "test:artifact:mcp": "AGENT_DRIVER_LIVE=1 bun test tests/driver-artifact-mcp.test.ts", "test:live": "vp run build && vp run test:live:artifact", "test:live:artifact": "AGENT_DRIVER_LIVE=1 AGENT_DRIVER_LIVE_SUITE=all bun test tests/driver-artifact-live.test.ts tests/driver-artifact-mcp.test.ts", "test:live:anthropic": "vp run build && AGENT_DRIVER_LIVE=1 AGENT_DRIVER_LIVE_SUITE=anthropic bun test tests/driver-artifact-live.test.ts", @@ -85,25 +86,26 @@ "build": "vp run build:types && bun build src/bin/driver.ts --target bun --outfile dist/driver.mjs", "build:types": "vp pack", "build:image": "vp run build && buildah build --http-proxy=false --platform linux/amd64 -t agent-driver:local .", + "check:generated": "bun scripts/sync-openai-generated.mjs --check", "test:image:environment": "podman run --pull=never --rm --entrypoint node agent-driver:local /usr/local/libexec/mosoo/environment-package-manager-check.mjs smoke", - "check": "vp check && vp run tc && vp run test && vp run build", + "check": "vp check && vp run check:generated && vp run tc && vp run test && vp run build && vp run test:artifact:mcp", "prepack": "vp run build" }, "dependencies": { "@agentclientprotocol/sdk": "1.4.0", "@anthropic-ai/claude-agent-sdk": "0.3.251", - "@anthropic-ai/sdk": "0.121.0", + "@anthropic-ai/sdk": "0.122.0", "@modelcontextprotocol/client": "^2.0.0", "@orpc/client": "^1.15.0", "fflate": "^0.8.3", "vestig": "^0.24.1", - "zod": "^4.4.3" + "zod": "4.5.4" }, "devDependencies": { - "@openai/codex": "0.150.1", + "@openai/codex": "0.151.0", "@types/bun": "1.4.0", "@types/node": "^26.3.0", - "opencode-ai": "1.18.23", + "opencode-ai": "1.18.25", "typescript": "^7.0.2", "vite-plus": "0.3.0" }, diff --git a/scripts/prune-openai-generated-types.mjs b/scripts/prune-openai-generated-types.mjs deleted file mode 100644 index 28a8127..0000000 --- a/scripts/prune-openai-generated-types.mjs +++ /dev/null @@ -1,100 +0,0 @@ -import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { dirname, relative, resolve, sep } from "node:path"; - -const generatedDir = resolve("src/runtimes/openai/generated"); -const schemaDir = resolve("src/runtimes/openai/generated-json-schema"); -const protocolTypesPath = resolve("src/runtimes/openai/app-server-protocol-types.ts"); -const methodsPath = resolve(generatedDir, "ProtocolMethods.ts"); - -function readMethods(schemaName) { - const schema = JSON.parse(readFileSync(resolve(schemaDir, `${schemaName}.json`), "utf8")); - if (!Array.isArray(schema.oneOf)) { - throw new TypeError(`${schemaName}.json must contain a oneOf array.`); - } - - const methods = schema.oneOf.map((branch, index) => { - const values = branch?.properties?.method?.enum; - if (values?.length !== 1 || typeof values[0] !== "string") { - throw new TypeError(`${schemaName}.json oneOf[${index}] must have one method enum value.`); - } - return values[0]; - }); - - if (new Set(methods).size !== methods.length) { - throw new TypeError(`${schemaName}.json contains duplicate methods.`); - } - return methods; -} - -function renderUnion(name, methods) { - return `export type ${name} =\n${methods.map((method) => ` | ${JSON.stringify(method)}`).join("\n")};`; -} - -writeFileSync( - methodsPath, - [ - "// GENERATED CODE! DO NOT MODIFY BY HAND!", - "", - "// Derived from the matching runtime JSON Schemas by scripts/prune-openai-generated-types.mjs.", - renderUnion("ServerNotificationMethod", readMethods("ServerNotification")), - "", - renderUnion("ServerRequestMethod", readMethods("ServerRequest")), - "", - ].join("\n"), -); - -function resolveImport(importer, specifier) { - const unresolved = resolve(dirname(importer), specifier); - const candidates = unresolved.endsWith(".ts") - ? [unresolved] - : [`${unresolved}.ts`, resolve(unresolved, "index.ts")]; - const file = candidates.find(existsSync); - if (file === undefined) { - throw new Error(`${relative(generatedDir, importer)} imports missing module ${specifier}.`); - } - - const path = relative(generatedDir, file); - if (path === ".." || path.startsWith(`..${sep}`)) { - throw new Error(`${relative(generatedDir, importer)} imports outside the generated directory.`); - } - return file; -} - -const protocolSource = readFileSync(protocolTypesPath, "utf8"); -const protocolImportPattern = /\bfrom\s+["'](\.\/generated(?:\/[^"']+)?)['"]/gu; -const pending = [...protocolSource.matchAll(protocolImportPattern)].map((match) => - resolveImport(protocolTypesPath, match[1]), -); -if (pending.length === 0) { - throw new Error("app-server-protocol-types.ts must import at least one generated type."); -} -const reachable = new Set(); -const relativeImportPattern = /\b(?:from\s+|import\s*\()\s*["'](\.[^"']+)["']/gu; - -while (pending.length > 0) { - const file = pending.pop(); - if (file === undefined || reachable.has(file)) { - continue; - } - reachable.add(file); - - for (const match of readFileSync(file, "utf8").matchAll(relativeImportPattern)) { - pending.push(resolveImport(file, match[1])); - } -} - -function listTypeScriptFiles(directory) { - return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { - const path = resolve(directory, entry.name); - return entry.isDirectory() ? listTypeScriptFiles(path) : path.endsWith(".ts") ? [path] : []; - }); -} - -const files = listTypeScriptFiles(generatedDir); -for (const file of files) { - if (!reachable.has(file)) { - rmSync(file); - } -} - -console.log(`Kept ${reachable.size} of ${files.length} generated TypeScript files.`); diff --git a/scripts/sync-openai-generated.mjs b/scripts/sync-openai-generated.mjs new file mode 100644 index 0000000..830c3ed --- /dev/null +++ b/scripts/sync-openai-generated.mjs @@ -0,0 +1,213 @@ +import { spawnSync } from "node:child_process"; +import { + copyFileSync, + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, relative, resolve, sep } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const generatedDir = resolve(root, "src/runtimes/openai/generated"); +const schemaDir = resolve(root, "src/runtimes/openai/generated-json-schema"); +const protocolTypesPath = resolve(root, "src/runtimes/openai/app-server-protocol-types.ts"); +const checkOnly = process.argv.includes("--check"); +const schemaSources = new Map([ + ["InitializeResponse.json", "v1/InitializeResponse.json"], + ["ServerNotification.json", "ServerNotification.json"], + ["ServerRequest.json", "ServerRequest.json"], + ["ThreadBackgroundTerminalsCleanResponse.json", "v2/ThreadBackgroundTerminalsCleanResponse.json"], + ["ThreadInjectItemsResponse.json", "v2/ThreadInjectItemsResponse.json"], + ["ThreadResumeResponse.json", "v2/ThreadResumeResponse.json"], + ["ThreadStartResponse.json", "v2/ThreadStartResponse.json"], + ["TurnStartResponse.json", "v2/TurnStartResponse.json"], +]); + +function run(command, args) { + const result = spawnSync(command, args, { cwd: root, encoding: "utf8" }); + if (result.status !== 0) { + throw new Error( + [result.stdout, result.stderr, `${command} exited with ${String(result.status)}.`] + .filter(Boolean) + .join("\n"), + ); + } +} + +function listFiles(directory, extension) { + return readdirSync(directory, { withFileTypes: true }) + .flatMap((entry) => { + const path = resolve(directory, entry.name); + return entry.isDirectory() + ? listFiles(path, extension) + : path.endsWith(extension) + ? [path] + : []; + }) + .sort(); +} + +function readMethods(stagedSchemaDir, schemaName) { + const schema = JSON.parse(readFileSync(resolve(stagedSchemaDir, `${schemaName}.json`), "utf8")); + if (!Array.isArray(schema.oneOf)) { + throw new TypeError(`${schemaName}.json must contain a oneOf array.`); + } + + const methods = schema.oneOf.map((branch, index) => { + const values = branch?.properties?.method?.enum; + if (values?.length !== 1 || typeof values[0] !== "string") { + throw new TypeError(`${schemaName}.json oneOf[${index}] must have one method enum value.`); + } + return values[0]; + }); + if (new Set(methods).size !== methods.length) { + throw new TypeError(`${schemaName}.json contains duplicate methods.`); + } + return methods; +} + +function renderUnion(name, methods) { + return `export type ${name} =\n${methods + .map((method) => ` | ${JSON.stringify(method)}`) + .join("\n")};`; +} + +function writeProtocolMethods(stagedSchemaDir, stagedTypesDir) { + writeFileSync( + resolve(stagedTypesDir, "ProtocolMethods.ts"), + [ + "// GENERATED CODE! DO NOT MODIFY BY HAND!", + "", + "// Derived from the matching runtime JSON Schemas by scripts/sync-openai-generated.mjs.", + renderUnion("ServerNotificationMethod", readMethods(stagedSchemaDir, "ServerNotification")), + "", + renderUnion("ServerRequestMethod", readMethods(stagedSchemaDir, "ServerRequest")), + "", + ].join("\n"), + ); +} + +function resolveImport(baseDir, importer, specifier) { + const unresolved = resolve(dirname(importer), specifier); + const candidates = unresolved.endsWith(".ts") + ? [unresolved] + : [`${unresolved}.ts`, resolve(unresolved, "index.ts")]; + const file = candidates.find(existsSync); + if (file === undefined) { + throw new Error(`${relative(baseDir, importer)} imports missing module ${specifier}.`); + } + + const path = relative(baseDir, file); + if (path === ".." || path.startsWith(`..${sep}`)) { + throw new Error(`${relative(baseDir, importer)} imports outside the generated directory.`); + } + return file; +} + +function pruneTypes(stagedTypesDir) { + const protocolSource = readFileSync(protocolTypesPath, "utf8"); + const protocolImportPattern = /\bfrom\s+["']\.\/generated(?:\/([^"']+))?["']/gu; + const protocolFile = resolve(stagedTypesDir, "__protocol__.ts"); + const pending = [...protocolSource.matchAll(protocolImportPattern)].map((match) => + resolveImport(stagedTypesDir, protocolFile, `./${match[1] ?? "index"}`), + ); + if (pending.length === 0) { + throw new Error("app-server-protocol-types.ts must import at least one generated type."); + } + + const reachable = new Set(); + const relativeImportPattern = /\b(?:from\s+|import\s*\()\s*["'](\.[^"']+)["']/gu; + while (pending.length > 0) { + const file = pending.pop(); + if (file === undefined || reachable.has(file)) { + continue; + } + reachable.add(file); + for (const match of readFileSync(file, "utf8").matchAll(relativeImportPattern)) { + pending.push(resolveImport(stagedTypesDir, file, match[1])); + } + } + + for (const file of listFiles(stagedTypesDir, ".ts")) { + if (!reachable.has(file)) { + rmSync(file); + } + } + return reachable.size; +} + +function replaceFiles(sourceDir, targetDir, extension) { + for (const file of listFiles(targetDir, extension)) { + rmSync(file); + } + for (const file of listFiles(sourceDir, extension)) { + const target = resolve(targetDir, relative(sourceDir, file)); + mkdirSync(dirname(target), { recursive: true }); + copyFileSync(file, target); + } +} + +function assertFilesMatch(expectedDir, actualDir, extension) { + const expected = listFiles(expectedDir, extension).map((file) => relative(expectedDir, file)); + const actual = listFiles(actualDir, extension).map((file) => relative(actualDir, file)); + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`Generated ${extension} file set is stale.`); + } + for (const path of expected) { + if ( + readFileSync(resolve(expectedDir, path), "utf8") !== + readFileSync(resolve(actualDir, path), "utf8") + ) { + throw new Error(`Generated file ${path} is stale.`); + } + } +} + +const temporaryRoot = mkdtempSync(join(tmpdir(), "mosoo-openai-generated-")); +try { + const completeSchemas = resolve(temporaryRoot, "complete-schemas"); + const completeTypes = resolve(temporaryRoot, "complete-types"); + const stagedSchemas = resolve(temporaryRoot, "schemas"); + const stagedTypes = resolve(temporaryRoot, "types"); + mkdirSync(stagedSchemas); + run(resolve(root, "node_modules/.bin/codex"), [ + "app-server", + "generate-json-schema", + "--experimental", + "--out", + completeSchemas, + ]); + run(resolve(root, "node_modules/.bin/codex"), [ + "app-server", + "generate-ts", + "--experimental", + "--out", + completeTypes, + ]); + + for (const [target, source] of schemaSources) { + copyFileSync(resolve(completeSchemas, source), resolve(stagedSchemas, target)); + } + cpSync(completeTypes, stagedTypes, { recursive: true }); + writeProtocolMethods(stagedSchemas, stagedTypes); + const retainedTypes = pruneTypes(stagedTypes); + run(resolve(root, "node_modules/.bin/vp"), ["fmt", stagedSchemas, stagedTypes]); + + if (checkOnly) { + assertFilesMatch(stagedSchemas, schemaDir, ".json"); + assertFilesMatch(stagedTypes, generatedDir, ".ts"); + console.log(`Verified 8 schemas and ${String(retainedTypes)} generated TypeScript files.`); + } else { + replaceFiles(stagedSchemas, schemaDir, ".json"); + replaceFiles(stagedTypes, generatedDir, ".ts"); + console.log(`Synchronized 8 schemas and ${String(retainedTypes)} generated TypeScript files.`); + } +} finally { + rmSync(temporaryRoot, { force: true, recursive: true }); +} diff --git a/src/bin/driver-process.ts b/src/bin/driver-process.ts index 1bb5349..8a6a7fc 100644 --- a/src/bin/driver-process.ts +++ b/src/bin/driver-process.ts @@ -3,6 +3,7 @@ import { createAgentDriverContext } from "../core/agent-driver-backend"; import { AgentBackendLifecycle } from "../core/agent-backend-lifecycle"; import { DriverCommandDispatcher } from "../core/driver-command-dispatcher"; import { deliverRunTerminal } from "../core/driver-command-delivery"; +import type { RunTerminalUpdate } from "../core/driver-command-delivery"; import { pushDriverDiagnosticEvent } from "../core/driver-diagnostics"; import { DriverHeartbeatLoop } from "../core/driver-heartbeat-loop"; import { DriverPermissionBroker } from "../core/driver-permission-broker"; @@ -45,8 +46,7 @@ export class DriverProcess { #backendLifecycle: AgentBackendLifecycle | null = null; #logger: Logger | null = null; #logUplink: DriverLogUplink | null = null; - #pendingRunCompletion = false; - #pendingRunFailure: RunError | null = null; + #pendingRunTerminal: RunTerminalUpdate | null = null; private readonly payload: DriverBootPayload; readonly #permissionBroker: DriverPermissionBroker; readonly #shutdownController = new AbortController(); @@ -228,7 +228,7 @@ export class DriverProcess { isShuttingDown: () => this.#runtimeState.isShuttingDown(), permissionRequests: this.#permissionBroker, rememberRunFailure: (error) => { - this.#pendingRunFailure ??= structuredClone(error); + this.rememberPendingRunTerminal({ error, status: "failed" }); this.rememberTerminalCause(new Error(error.message, { cause: error })); }, runtimeContextFactory: (_runtimeSocket, runtimeLogger) => @@ -251,12 +251,15 @@ export class DriverProcess { if (!shutdownAbort) { this.rememberTerminalCause(error); const failure = this.#terminalCause?.error ?? error; - this.#pendingRunFailure ??= { - code: "driver.runtime_failed", - details: {}, - message: failure instanceof Error ? failure.message : "Driver runtime failed.", - retryable: false, - }; + this.rememberPendingRunTerminal({ + error: { + code: "driver.runtime_failed", + details: {}, + message: failure instanceof Error ? failure.message : "Driver runtime failed.", + retryable: false, + }, + status: "failed", + }); if (!this.#runtimeState.isShuttingDown()) { this.#runtimeState.enter("failed"); } @@ -307,19 +310,16 @@ export class DriverProcess { socket.currentRunId() !== null && (reason === "signal.sigint" || reason === "signal.sigterm") ) { - this.#pendingRunCompletion = true; + this.rememberPendingRunTerminal({ status: "completed" }); } if (this.#runtimeState.status() !== "failed" && this.#runtimeState.status() !== "stopped") { this.#runtimeState.enter("stopping"); } - const permissionPending = this.#permissionBroker.hasPending(); const permissionCancellation = this.#permissionBroker.rejectAllAndWait(); this.#shutdownController.abort(new Error(reason)); socket.abortConnect(reason); - // Keep the RPC lane open long enough to publish the lossless permission cancellation. - if (!permissionPending) { - socket.abortPendingRequests(reason); - } + // Backend cleanup owns lossless terminal and committed-file reports, so the + // RPC lane remains open until that ownership barrier settles. // If hello never completed, a gated flush may still be pending; open the // gate so log teardown cannot hang shutdown. @@ -341,6 +341,7 @@ export class DriverProcess { } const backendShutdown = await Promise.allSettled([this.#backendLifecycle?.shutdown(reason)]); + socket.abortPendingRequests(reason); const failure = backendShutdown.find((result) => result.status === "rejected"); if (permissionFailure !== null) { @@ -453,15 +454,13 @@ export class DriverProcess { } } - if (this.#pendingRunFailure !== null && shutdownFailure === null) { + if (this.#pendingRunTerminal !== null && shutdownFailure === null) { try { - await this.reportRunFailure(socket, this.#pendingRunFailure); - } catch (error) { - terminalFailure = { error }; - } - } else if (this.#pendingRunCompletion && shutdownFailure === null) { - try { - await deliverRunTerminal(socket, { status: "completed" }); + if (this.#pendingRunTerminal.status === "failed") { + await this.reportRunFailure(socket, this.#pendingRunTerminal.error); + } else { + await deliverRunTerminal(socket, this.#pendingRunTerminal); + } } catch (error) { terminalFailure = { error }; } @@ -525,7 +524,7 @@ export class DriverProcess { ports: { mcp: { prepare: (command, signal) => - prepareRemoteHttpMcpCommand(this.#startInput, command, signal), + prepareRemoteHttpMcpCommand(this.#startInput, command, signal, logger), }, skill: { materialize: async (execution, signal) => @@ -553,6 +552,13 @@ export class DriverProcess { this.#terminalCause ??= { error }; } + private rememberPendingRunTerminal(terminal: RunTerminalUpdate): void { + if (this.#pendingRunTerminal?.status === "failed") { + return; + } + this.#pendingRunTerminal = structuredClone(terminal); + } + private throwTerminalCause(): void { if (this.#terminalCause !== null) { throw this.#terminalCause.error; diff --git a/src/core/agent-driver-backend.ts b/src/core/agent-driver-backend.ts index 09df56e..c20270d 100644 --- a/src/core/agent-driver-backend.ts +++ b/src/core/agent-driver-backend.ts @@ -68,11 +68,13 @@ function toAgentDriverEventSink( throw new Error("Driver external tool effect ledger is not configured."); }, commandUpdate: async () => {}, - completeExternalToolEffect: async () => { + observeExternalToolEffect: async () => { throw new Error("Driver external tool effect ledger is not configured."); }, currentRunId: eventSink.currentRunId.bind(eventSink), - markExternalToolEffectUnknown: async () => {}, + settleExternalToolEffect: async () => { + throw new Error("Driver external tool effect ledger is not configured."); + }, pushEvents: (input) => eventSink.pushEvents(input), }; } @@ -90,7 +92,7 @@ function createDefaultHostPorts(input: AgentDriverContextInput): AgentDriverHost }), eventSink, file: { - reportChanged: async (fileChange) => { + reportChanged: async (fileChange, signal) => { const event = { kind: "file.changed", payload: { @@ -100,7 +102,7 @@ function createDefaultHostPorts(input: AgentDriverContextInput): AgentDriverHost }, } satisfies DriverEventInput; - await pushLosslessEvents(eventSink, [event]); + await pushLosslessEvents(eventSink, [event], undefined, signal); }, }, mcp: { diff --git a/src/core/agent-driver-kernel.ts b/src/core/agent-driver-kernel.ts index 66ec98a..c2b2c18 100644 --- a/src/core/agent-driver-kernel.ts +++ b/src/core/agent-driver-kernel.ts @@ -2,10 +2,15 @@ import { createDisabledLogger } from "../observability"; import type { Logger } from "../observability"; import type { DriverEventInput } from "../protocol/events"; import { createDriverId, type RunId } from "../protocol/id"; -import type { DriverEventBatchOutput } from "../protocol/orpc"; +import type { DriverEventBatchOutput, DriverEventReceipt } from "../protocol/orpc"; import type { DriverStartInput } from "../protocol/start"; -import { parseRuntimeCommand } from "../runtime-command"; -import type { RunError, RuntimeCommand, RuntimeCommandResult } from "../runtime-command"; +import { normalizeDurableRunError, parseRuntimeCommand } from "../runtime-command"; +import type { + DriverCommandUpdate, + RunError, + RuntimeCommand, + RuntimeCommandResult, +} from "../runtime-command"; import { AgentBackendLifecycle } from "./agent-backend-lifecycle"; import type { AgentDriverBackendFactory, @@ -13,12 +18,28 @@ import type { AgentDriverContextPortOverrides, } from "./agent-driver-backend"; import { createAgentDriverContext } from "./agent-driver-backend"; +import type { AgentDriverEventSink } from "../host-ports"; import { AsyncValueQueue } from "./async-value-queue"; import { DriverCommandDispatcher } from "./driver-command-dispatcher"; import { DriverPermissionBroker } from "./driver-permission-broker"; import { createDriverPermissionRequestHandler } from "./driver-permission-policy"; -import type { DriverRuntimeExternalToolEffectPort, DriverRuntimeIo } from "./driver-runtime-io"; +import { + assertDriverEventReceiptPrefix, + assertIsolatedRunTerminalBatch, + withSourceEventIds, + type DriverRuntimeExternalToolEffectPort, + type DriverRuntimeIo, + type DriverRunTerminalBarrier, +} from "./driver-runtime-io"; import { DriverRuntimeStateMachine } from "./driver-runtime-state"; +import { + DriverTerminalStateMachine, + type DriverInputOutcome, + type DriverInputSettlement, + type DriverRunSnapshot, + type DriverRunTerminalIdentity, + type DriverRunTicket, +} from "./driver-terminal-state"; export type AgentDriverKernelStartInput = DriverStartInput; export type AgentDriverRuntimeEvent = DriverEventInput; @@ -44,10 +65,6 @@ export interface AgentDriverKernelOptions { } type KernelCommandResult = RuntimeCommandResult | void; -type RunEventTerminal = { - readonly runId: RunId; - readonly status: "cancelled" | "completed" | "failed"; -}; const KERNEL_SHUTDOWN_TIMEOUT_MS = 5_000; const KERNEL_START_TIMEOUT_MS = 60_000; @@ -60,11 +77,7 @@ function jsonBytes(value: unknown): number { return Buffer.byteLength(JSON.stringify(value), "utf8"); } -function toDispatchError(error: RunError | undefined, command: RuntimeCommand): Error { - if (!error) { - return new Error(`Driver command ${command.kind} failed.`); - } - +function toDispatchError(error: RunError): Error { const dispatchError = new Error(error.message); dispatchError.name = error.code; return dispatchError; @@ -96,16 +109,17 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo readonly #permissionBroker: DriverPermissionBroker; readonly #runtimeState = new DriverRuntimeStateMachine("created"); readonly #shutdownController = new AbortController(); - #activeRunId: RunId | null = null; + readonly #terminalState = new DriverTerminalStateMachine(); #backendLifecycle: AgentBackendLifecycle | null = null; #eventFinalizationTask: Promise | null = null; + #initialRunId: RunId | null = null; #pushedEventSeq = 0; + #runTicket: DriverRunTicket | null = null; #runTask: Promise | null = null; #runTaskSettled = false; - #runEventTerminal: RunEventTerminal | null = null; - #shutdownComplete = false; - #shutdownRunFailure: { error: RunError; runId: RunId | null } | null = null; - #runTerminal: "cancelled" | "completed" | "failed" | null = null; + #runEventTerminalTask: { task: Promise; ticket: DriverRunTicket } | null = + null; + #runTerminalBarrier: DriverRunTerminalBarrier | null = null; #shutdownTask: Promise | null = null; #stopTask: Promise | null = null; #terminalCause: { error: unknown } | null = null; @@ -118,32 +132,35 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo this.#permissionBroker = new DriverPermissionBroker(() => this.#logger); } - beginRun(runId: RunId): void { - if (this.#activeRunId !== null) { - throw new Error(`Driver run ${this.#activeRunId} is already active.`); - } - this.#activeRunId = runId; - this.#runEventTerminal = null; - this.#runTerminal = null; + beginRun(runId: RunId): DriverRunTicket { + const ticket = this.#terminalState.beginRun(runId); + this.#runTicket = ticket; + return ticket; + } + + claimRunCancellation( + ticket: DriverRunTicket, + reason: string, + ): "already_claimed" | "claimed" | "terminal_selected" { + return this.#terminalState.claimCancellation(ticket, reason); } async cancel(reason: string): Promise { + const runId = this.currentRunId(); + + if (runId === null) { + throw new Error("Driver has no active run to cancel."); + } + await this.dispatch({ commandId: createDriverId(), kind: "turn.cancel", reason, + runId, }); } - async commandUpdate( - input: { - commandId: string; - error?: RunError; - result?: RuntimeCommandResult; - status: "accepted" | "cancelled" | "completed" | "failed"; - }, - _signal: AbortSignal, - ): Promise { + async commandUpdate(input: DriverCommandUpdate, _signal: AbortSignal): Promise { const update = structuredClone(input); if (update.status === "accepted") { return; @@ -160,11 +177,11 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo this.#commandResults.delete(update.commandId); if (update.status === "failed") { - result.reject(toDispatchError(update.error, command)); + result.reject(toDispatchError(update.error)); return; } - result.resolve(update.result === undefined ? undefined : update.result); + result.resolve(update.status === "completed" ? update.result : undefined); } async claimExternalToolEffect( @@ -174,24 +191,43 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo return this.#requireExternalToolEffectLedger().claimExternalToolEffect(input, signal); } - async completeExternalToolEffect( - input: Parameters[0], + async observeExternalToolEffect( + input: Parameters[0], signal: AbortSignal, - ): Promise { - await this.#requireExternalToolEffectLedger().completeExternalToolEffect(input, signal); + ): ReturnType { + return this.#requireExternalToolEffectLedger().observeExternalToolEffect(input, signal); } async completeRun(): Promise { - if (this.#runTerminal !== null) { + const runId = this.#terminalState.terminalRunId(this.#initialRunId); + if (runId === null) { + throw new Error("Driver run terminal requires an exact run ID."); + } + const terminal = { runId, status: "completed" } as const; + const selection = this.#terminalState.selectInstanceTerminal(terminal); + if (selection === "acked") { return; } - this.#pushTerminalEvent({ - kind: "run.completed", - payload: { - stopReason: "end_turn", - }, - }); - this.#runTerminal = "completed"; + + try { + if ( + this.#terminalState.currentRunId() === null && + this.#terminalState.acknowledgedRunTerminal() === null + ) { + this.#pushTerminalEvent({ + kind: "run.completed", + payload: { + stopReason: "end_turn", + }, + }); + } + } catch (error) { + if (selection === "selected") { + this.#terminalState.abandonInstanceTerminal(terminal); + } + throw error; + } + this.#terminalState.ackInstanceTerminal(terminal); } async dispatch(command: RuntimeCommand): Promise { @@ -209,9 +245,10 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo return result.promise; } - endRun(runId: RunId): void { - if (this.#activeRunId === runId) { - this.#activeRunId = null; + releaseRun(ticket: DriverRunTicket, reason: "command_acked" | "driver_failing"): void { + this.#terminalState.releaseRun(ticket, reason); + if (this.#runTicket === ticket) { + this.#runTicket = null; } } @@ -220,22 +257,43 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } async failRun(error: RunError): Promise { - this.#publishRunFailure(error, this.#activeRunId); + this.#publishRunFailure(error, this.currentRunId()); } #publishRunFailure(error: RunError, runId: RunId | null): void { - if (this.#runTerminal !== null) { + const durableError = normalizeDurableRunError(error); + const exactRunId = this.#terminalState.terminalRunId(runId ?? this.#initialRunId); + if (exactRunId === null) { + throw new Error("Driver run terminal requires an exact run ID."); + } + const terminal = { + error: structuredClone(durableError), + runId: exactRunId, + status: "failed", + } as const; + const selection = this.#terminalState.selectInstanceTerminal(terminal); + if (selection === "acked") { return; } - this.#pushTerminalEvent({ - kind: "run.failed", - payload: { - error, - recoverable: false, - }, - ...(runId === null ? {} : { runId }), - }); - this.#runTerminal = "failed"; + + try { + if (this.#terminalState.acknowledgedRunTerminal(exactRunId) === null) { + this.#pushTerminalEvent({ + kind: "run.failed", + payload: { + error: durableError, + recoverable: false, + }, + runId: exactRunId, + }); + } + } catch (publishError) { + if (selection === "selected") { + this.#terminalState.abandonInstanceTerminal(terminal); + } + throw publishError; + } + this.#terminalState.ackInstanceTerminal(terminal); } async heartbeat( @@ -253,31 +311,63 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } currentRunId(): RunId | null { - return this.#activeRunId; + return this.#terminalState.currentRunId(); } - async markExternalToolEffectUnknown( - input: Parameters[0], + async settleExternalToolEffect( + input: Parameters[0], signal: AbortSignal, - ): Promise { - await this.#requireExternalToolEffectLedger().markExternalToolEffectUnknown(input, signal); - } - async pushEvents(input: { events: DriverEventInput[] }): Promise { - const events = structuredClone(input.events); - let terminal: RunEventTerminal | null = null; + ): ReturnType { + return this.#requireExternalToolEffectLedger().settleExternalToolEffect(input, signal); + } + + async pushEvents(input: { + events: DriverEventInput[]; + signal?: AbortSignal; + }): Promise { + return this.#pushEvents(input); + } + + async #pushEvents( + input: { events: DriverEventInput[]; signal?: AbortSignal }, + eventSink?: AgentDriverEventSink, + ): Promise { + input.signal?.throwIfAborted(); + const ticket = this.#runTicket; + const selectedTerminal = + ticket === null ? null : this.#terminalState.snapshotRun(ticket.runId)?.terminal?.value; + const ownedEvents = input.events.map((event) => + selectedTerminal !== null && + selectedTerminal !== undefined && + event.sourceEventId === undefined && + (event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed") + ? { ...event, sourceEventId: selectedTerminal.sourceEventId } + : event, + ); + const events = structuredClone(withSourceEventIds(ownedEvents)); + assertIsolatedRunTerminalBatch(events); + const barrier = this.#runTerminalBarrier; + if (barrier !== null) { + const pending = barrier(events); + if (pending !== undefined) { + await pending; + } + } + const activeRunId = this.currentRunId(); + let hasRunScopedEvent = false; + let terminal: DriverRunTerminalIdentity | null = null; let terminalIndex = -1; for (const [index, event] of events.entries()) { - const runId = event.runId === undefined ? this.#activeRunId : event.runId; + const runId = event.runId === undefined ? activeRunId : event.runId; if (runId !== null) { - if (runId !== this.#activeRunId) { + if (runId !== activeRunId) { throw new Error("Driver event must target the active run."); } - - if (this.#runTerminal !== null) { - throw new Error("Driver event cannot target a terminated run."); - } + hasRunScopedEvent = true; } const status = @@ -305,7 +395,12 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo throw new Error("Driver event batch cannot contain multiple run terminals."); } - terminal = { runId, status }; + terminal = { + event, + runId, + sourceEventId: event.sourceEventId!, + status, + }; terminalIndex = index; } @@ -313,27 +408,116 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo throw new Error("Driver run terminal must be the final event in its batch."); } - this.#events.pushMany(events); - + let terminalSelection: "acked" | "cancelled" | "pending" | "selected" | null = null; if (terminal !== null) { - this.#runEventTerminal = terminal; - this.#runTerminal = terminal.status; + if (ticket === null) { + throw new Error("Driver run terminal must target the active run."); + } + + terminalSelection = this.#terminalState.selectRunTerminal(ticket, terminal); + if (terminalSelection === "cancelled") { + ticket.signal.throwIfAborted(); + throw new Error("Driver run terminal lost to cancellation."); + } + if (terminalSelection === "acked") { + const selected = this.#terminalState.snapshotRun(ticket.runId)?.terminal; + if (events.length !== 1 || selected?.phase !== "acked") { + throw new Error("Driver acknowledged terminal retry must contain only that terminal."); + } + return { accepted: [selected.receipt] }; + } + if (terminalSelection === "pending" && this.#runEventTerminalTask !== null) { + if (this.#runEventTerminalTask.ticket !== ticket) { + throw new Error("Driver active run changed during terminal delivery."); + } + return { accepted: [await this.#runEventTerminalTask.task] }; + } + } else if (hasRunScopedEvent && this.#terminalState.snapshotRun()?.terminal !== null) { + throw new Error("Driver event cannot target a terminated run."); } - const accepted = events.map((event) => { - this.#pushedEventSeq += 1; + let delivery: Promise; + if (eventSink === undefined) { + try { + this.#events.pushMany(events); + } catch (error) { + if (terminal !== null && terminalSelection === "selected") { + this.#terminalState.abandonRunTerminal(ticket!, terminal); + } + throw error; + } + delivery = Promise.resolve({ + accepted: events.map((event) => { + this.#pushedEventSeq += 1; + return { + eventId: event.sourceEventId!, + seq: this.#pushedEventSeq, + type: event.kind, + }; + }), + }); + } else { + delivery = eventSink.pushEvents({ + events, + ...(input.signal === undefined ? {} : { signal: input.signal }), + }); + } - return { - seq: this.#pushedEventSeq, - type: event.kind, - }; - }); + const terminalTask = + terminal === null || ticket === null + ? null + : { + task: delivery.then((result) => { + assertDriverEventReceiptPrefix(events, result.accepted); + if (result.accepted.length !== events.length) { + throw new Error("Driver run terminal was not fully acknowledged."); + } + const receipt = result.accepted[terminalIndex]; + if (receipt === undefined) { + throw new Error("Driver run terminal receipt is missing."); + } + this.#terminalState.ackRunTerminal(ticket, receipt); + return receipt; + }), + ticket, + }; + + if (terminalTask !== null) { + this.#runEventTerminalTask = terminalTask; + void terminalTask.task.catch(() => {}); + } - return { accepted }; + try { + const result = await delivery; + assertDriverEventReceiptPrefix(events, result.accepted); + await terminalTask?.task; + return result; + } finally { + if (this.#runEventTerminalTask === terminalTask) { + this.#runEventTerminalTask = null; + } + } + } + + registerRunTerminalBarrier(barrier: DriverRunTerminalBarrier): () => void { + if (this.#runTerminalBarrier !== null) { + throw new Error("Driver run terminal barrier is already registered."); + } + + this.#runTerminalBarrier = barrier; + return () => { + if (this.#runTerminalBarrier === barrier) { + this.#runTerminalBarrier = null; + } + }; } - runEventTerminal(runId: RunId): "cancelled" | "completed" | "failed" | null { - return this.#runEventTerminal?.runId === runId ? this.#runEventTerminal.status : null; + runSnapshot(runId?: RunId): DriverRunSnapshot | null { + return this.#terminalState.snapshotRun(runId); + } + + settleRunInput(ticket: DriverRunTicket, outcome: DriverInputOutcome): DriverInputSettlement { + return this.#terminalState.settleInput(ticket, outcome); } async start(input: AgentDriverKernelStartInput): Promise { @@ -342,6 +526,7 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } const admitted = structuredClone(input); + this.#initialRunId = admitted.execution.run.runId; this.#runtimeState.enter("starting"); await this.#start(admitted); } @@ -509,9 +694,15 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } #createContext(payload: DriverStartInput): AgentDriverContext { + const customEventSink = this.#hostPorts?.eventSink; + const hostPorts = + customEventSink === undefined + ? this.#hostPorts + : { ...this.#hostPorts, eventSink: this.#guardEventSink(customEventSink) }; + return createAgentDriverContext({ eventSink: this, - ...(this.#hostPorts === undefined ? {} : { ports: this.#hostPorts }), + ...(hostPorts === undefined ? {} : { ports: hostPorts }), lifecycle: { fail: (error) => this.#onBackendFailure(error), }, @@ -539,6 +730,39 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo }); } + #guardEventSink(eventSink: AgentDriverEventSink): AgentDriverEventSink { + const claimExternalToolEffect = eventSink.claimExternalToolEffect; + const observeExternalToolEffect = eventSink.observeExternalToolEffect; + const settleExternalToolEffect = eventSink.settleExternalToolEffect; + + return { + ...(claimExternalToolEffect === undefined + ? {} + : { + claimExternalToolEffect: (input, signal) => + claimExternalToolEffect.call(eventSink, input, signal), + }), + commandUpdate: async (input, signal) => { + await eventSink.commandUpdate(input, signal); + await this.commandUpdate(input, signal); + }, + currentRunId: () => this.currentRunId(), + ...(observeExternalToolEffect === undefined + ? {} + : { + observeExternalToolEffect: (input, signal) => + observeExternalToolEffect.call(eventSink, input, signal), + }), + pushEvents: (input) => this.#pushEvents(input, eventSink), + ...(settleExternalToolEffect === undefined + ? {} + : { + settleExternalToolEffect: (input, signal) => + settleExternalToolEffect.call(eventSink, input, signal), + }), + }; + } + #requireExternalToolEffectLedger(): DriverRuntimeExternalToolEffectPort { if (this.#externalToolEffectLedger === undefined) { throw new Error( @@ -572,7 +796,7 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo this.#runtimeState.enter("failed"); this.#rejectPendingCommands(error); - if (this.#activeRunId !== null) { + if (this.currentRunId() !== null) { this.#rememberRunFailure({ code: "driver.runtime_failed", details: {}, @@ -600,10 +824,7 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } #rememberRunFailure(error: RunError): void { - this.#shutdownRunFailure ??= { - error: structuredClone(error), - runId: this.#activeRunId, - }; + this.#terminalState.recordFailure(error); } #pushTerminalEvent(event: AgentDriverRuntimeEvent): void { @@ -627,8 +848,14 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } async #finalizeEvents(): Promise { - if (this.#shutdownRunFailure !== null) { - this.#publishRunFailure(this.#shutdownRunFailure.error, this.#shutdownRunFailure.runId); + const failure = this.#terminalState.shutdownSnapshot()?.failure; + const instanceTerminal = this.#terminalState.snapshotInstance(); + if ( + failure !== null && + failure !== undefined && + (instanceTerminal.phase === "open" || instanceTerminal.terminal.status === "failed") + ) { + this.#publishRunFailure(failure.error, failure.runId); } await this.#closeEventsAfterPermissions(); } @@ -638,12 +865,15 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo this.#runtimeState.enter("stopped"); } this.#backendLifecycle = null; - this.#shutdownComplete = true; + this.#terminalState.markCleanupCompleted(); await this.#finalizeShutdown(); } async #finalizeShutdown(): Promise { - if (!this.#shutdownComplete || (this.#runTask !== null && !this.#runTaskSettled)) { + if ( + this.#terminalState.shutdownSnapshot()?.cleanup !== "completed" || + (this.#runTask !== null && !this.#runTaskSettled) + ) { return; } @@ -661,6 +891,7 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo } async #runShutdown(reason: string): Promise { + this.#terminalState.requestShutdown(); if (this.#runtimeState.status() === "stopped") { await this.#finalizeShutdown(); return; diff --git a/src/core/driver-command-delivery.ts b/src/core/driver-command-delivery.ts index 16bd819..8564bd2 100644 --- a/src/core/driver-command-delivery.ts +++ b/src/core/driver-command-delivery.ts @@ -14,36 +14,25 @@ const COMMAND_UPDATE_TIMEOUT_MS = 1_000; const RUN_TERMINAL_UPDATE_ATTEMPT_TIMEOUT_MS = 250; const TERMINAL_UPDATE_MAX_ATTEMPTS = 3; -export interface TerminalCommandUpdate { - error?: RunError; - result?: RuntimeCommandResult; - status: "cancelled" | "completed" | "failed"; -} - -export type RunTerminalUpdate = +export type TerminalCommandUpdate = + | { readonly status: "cancelled" } | { - status: "completed"; + readonly result?: Exclude | undefined; + readonly status: "completed"; } - | { - error: RunError; - status: "failed"; - }; + | { readonly error: RunError; readonly status: "failed" }; -type RunTerminalDelivery = +export type RunTerminalUpdate = | { - delivered: boolean; status: "completed"; - task?: Promise; } | { - delivered: boolean; error: RunError; status: "failed"; - task?: Promise; }; export interface TrackedCommand { - readonly command: RuntimeCommand; + readonly identity: unknown; delivery: Promise; terminal?: TerminalCommandUpdate; terminalTask?: Promise; @@ -70,25 +59,19 @@ function toErrorMessage(error: unknown, fallback: string): string { async function sendCommandUpdate( runtimeContext: AgentDriverContext, command: RuntimeCommand, - update: { - error?: RunError; - result?: RuntimeCommandResult; - status: "accepted" | "cancelled" | "completed" | "failed"; - }, + update: { readonly status: "accepted" } | TerminalCommandUpdate, signal: AbortSignal, ): Promise { - const delivery = structuredClone({ - commandId: command.commandId, - ...(update.error === undefined ? {} : { error: update.error }), - ...(update.result === undefined ? {} : { result: update.result }), - status: update.status, - }); + const delivery = structuredClone({ commandId: command.commandId, ...update }); await raceWithAbort(runtimeContext.ports.eventSink.commandUpdate(delivery, signal), signal); runtimeContext.logger.debug("driver.runtime.command.status.sent", { command: summarizeRuntimeCommand(command), - ...(update.error ? { error: update.error } : {}), - result: update.result ? summarizeRuntimeCommandResult(update.result) : null, + ...(update.status === "failed" ? { error: update.error } : {}), + result: + update.status === "completed" && update.result !== undefined + ? summarizeRuntimeCommandResult(update.result) + : null, status: update.status, }); } @@ -96,22 +79,16 @@ async function sendCommandUpdate( export class DriverCommandDelivery { readonly #shutdownSignal: AbortSignal; readonly #trackedCommands = new Map(); - #runTerminal: RunTerminalDelivery | null = null; constructor(shutdownSignal: AbortSignal) { this.#shutdownSignal = shutdownSignal; } - receive(command: RuntimeCommand): CommandReceipt { - const tracked = this.#trackedCommands.get(command.commandId); + receive(command: RuntimeCommand, identity: unknown = command): CommandReceipt { + const replay = this.replay(command, identity); - if (tracked !== undefined) { - if (!isDeepStrictEqual(tracked.command, command)) { - throw new Error( - `Driver command ${command.commandId} was replayed with changed identity or content.`, - ); - } - return { replay: true, tracked }; + if (replay !== null) { + return replay; } if (this.#trackedCommands.size >= MAX_TRACKED_COMMANDS) { @@ -119,19 +96,37 @@ export class DriverCommandDelivery { } const added: TrackedCommand = { - command: structuredClone(command), delivery: Promise.resolve(), + identity: structuredClone(identity), }; this.#trackedCommands.set(command.commandId, added); return { replay: false, tracked: added }; } + replay(command: RuntimeCommand, identity: unknown = command): CommandReceipt | null { + const tracked = this.#trackedCommands.get(command.commandId); + + if (tracked !== undefined) { + if (!isDeepStrictEqual(tracked.identity, identity)) { + throw new Error( + `Driver command ${command.commandId} was replayed with changed identity or content.`, + ); + } + return { replay: true, tracked }; + } + return null; + } + hasTerminal(commandId: string): boolean { return this.#trackedCommands.get(commandId)?.terminal !== undefined; } - resetRunTerminal(): void { - this.#runTerminal = null; + reject( + runtimeContext: AgentDriverContext, + command: RuntimeCommand, + update: TerminalCommandUpdate, + ): Promise { + return this.#deliverTerminal(runtimeContext, command, structuredClone(update)); } accept( @@ -199,59 +194,6 @@ export class DriverCommandDelivery { await task; } - claimRunTerminal( - socket: DriverRuntimeIo, - status: "completed" | "failed", - error?: RunError, - ): Promise { - let terminal = this.#runTerminal; - - if (terminal === null) { - if (status === "failed" && error === undefined) { - return Promise.reject(new Error("Failed run terminal requires an error.")); - } - - terminal = - status === "completed" - ? { delivered: false, status } - : { delivered: false, error: structuredClone(error!), status }; - this.#runTerminal = terminal; - } - - if (terminal.status !== status) { - return Promise.resolve(); - } - if ( - terminal.status === "failed" && - (error === undefined || !isDeepStrictEqual(terminal.error, error)) - ) { - return Promise.reject(new Error("Failed run terminal was retried with a different error.")); - } - if (terminal.delivered) { - return Promise.resolve(); - } - if (terminal.task !== undefined) { - return terminal.task; - } - - const task = deliverRunTerminal(socket, terminal); - terminal.task = task; - void task.then( - () => { - if (terminal.task === task) { - terminal.delivered = true; - delete terminal.task; - } - }, - () => { - if (terminal.task === task) { - delete terminal.task; - } - }, - ); - return task; - } - async #deliverTerminal( runtimeContext: AgentDriverContext, command: RuntimeCommand, diff --git a/src/core/driver-command-dispatcher.ts b/src/core/driver-command-dispatcher.ts index d43786d..53f125d 100644 --- a/src/core/driver-command-dispatcher.ts +++ b/src/core/driver-command-dispatcher.ts @@ -1,28 +1,47 @@ +import { createHash, randomUUID } from "node:crypto"; + +import { AGENT_DRIVER_MCP_EXECUTE_TIMEOUT_MS } from "../host-ports"; import { createScopedWideEvent, emitWideEvent } from "../observability"; import type { Logger } from "../observability"; import { summarizeRuntimeCommand } from "../observability/driver-debug"; +import type { DriverEventInput } from "../protocol/events"; import { parseRunId } from "../protocol/id"; import type { RunId } from "../protocol/id"; -import type { McpExecuteCommandResult, RunError, RuntimeCommand } from "../runtime-command"; +import type { + McpExecuteCommandResult, + McpExternalToolEffectSettlement, + McpExternalToolEffectState, + RunError, + RuntimeCommand, +} from "../runtime-command"; +import { + createMcpUnknownEffectRunError, + createMcpUnsettledEffectRunError, + normalizeDurableRunError, + parseRuntimeCommand, +} from "../runtime-command"; import { promiseWithTimeout, raceWithAbort, sleepPromise } from "../utils/async"; import type { AgentDriverBackend, AgentDriverContext } from "./agent-driver-backend"; -import { DriverCommandDelivery, TerminalCommandDeliveryError } from "./driver-command-delivery"; +import { + type CommandReceipt, + deliverRunTerminal, + DriverCommandDelivery, + TerminalCommandDeliveryError, +} from "./driver-command-delivery"; import { pushDriverDiagnosticEvent } from "./driver-diagnostics"; +import type { DriverPermissionBroker } from "./driver-permission-broker"; import { - PermissionEventDeliveryError, - type DriverPermissionBroker, -} from "./driver-permission-broker"; + createDurableMcpSucceededSettlement, + requireDurableMcpResultIdentity, +} from "./external-tool-effect-settlement"; import { DRIVER_EVENT_DELIVERY_TIMEOUT_MS, pushLosslessEvents, type DriverRuntimeIo, } from "./driver-runtime-io"; import type { DriverRuntimeStateMachine } from "./driver-runtime-state"; -import { - DriverTurnCancellationCleanupError, - DriverTurnCancelledError, - isDriverTurnCancelledError, -} from "./driver-runtime-state"; +import { isDriverTurnCancelledError } from "./driver-runtime-state"; +import type { DriverInputOutcome, DriverRunTicket } from "./driver-terminal-state"; interface DriverCommandDispatcherOptions { backend: AgentDriverBackend; @@ -41,19 +60,36 @@ const COMMAND_POLL_INTERVAL_MS = 250; export const ACTIVE_TURN_CANCEL_GRACE_MS = 2_000; const ACTIVE_INPUT_SETTLE_GRACE_MS = ACTIVE_TURN_CANCEL_GRACE_MS + DRIVER_EVENT_DELIVERY_TIMEOUT_MS; const EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS = 2_000; +export const ACTIVE_MCP_COMMIT_GRACE_MS = AGENT_DRIVER_MCP_EXECUTE_TIMEOUT_MS + 30_000; const MAX_ACTIVE_MCP_COMMANDS = 32; interface ActiveMcpCommand { readonly controller: AbortController; + readonly runId: RunId; readonly task: Promise; } +interface ActiveMcpAdmission { + readonly runId: RunId; + readonly settled: Promise; + settle(): void; +} + +interface RunTerminalEvent { + readonly kind: "run.cancelled" | "run.completed" | "run.failed"; + readonly runId: RunId; +} + function isFatalCommand(command: RuntimeCommand): boolean { return command.kind === "input.start" || command.kind === "turn.cancel"; } function toCommandFailure(command: RuntimeCommand, error: unknown): RunError { - return { + if (error instanceof ExternalToolEffectUnknownError) { + return normalizeDurableRunError(error.failure); + } + + return normalizeDurableRunError({ code: `driver.command_failed.${command.kind}`, details: { commandId: command.commandId, @@ -61,19 +97,44 @@ function toCommandFailure(command: RuntimeCommand, error: unknown): RunError { }, message: error instanceof Error ? error.message : `Driver command ${command.kind} failed.`, retryable: false, - }; + }); } function toErrorMessage(error: unknown, fallback: string): string { return error instanceof Error ? error.message : fallback; } -class ExternalToolEffectResolutionRequiredError extends Error { +class ExternalToolEffectUnknownError extends Error { + readonly failure: RunError; + constructor(command: Extract, effectId: string) { - super( - `External effect ${effectId} for MCP tool ${command.toolName} is unknown. Verify the provider outcome and explicitly decide whether to create a new action; this command will never replay automatically.`, - ); - this.name = "ExternalToolEffectResolutionRequiredError"; + const failure = createMcpUnknownEffectRunError(command, effectId); + super(failure.message); + this.name = "ExternalToolEffectUnknownError"; + this.failure = failure; + } +} + +class ExternalToolEffectUnsettledError extends Error { + readonly failure: RunError; + + constructor( + command: Extract, + effectId: string, + cause?: unknown, + ) { + const failure = createMcpUnsettledEffectRunError(command, effectId); + super(failure.message, cause === undefined ? undefined : { cause }); + this.name = "ExternalToolEffectUnsettledError"; + this.failure = failure; + } +} + +async function retryOnce(operation: () => Promise): Promise { + try { + return await operation(); + } catch { + return operation(); } } @@ -97,8 +158,11 @@ export class DriverCommandDispatcher { readonly #shutdown: (socket: DriverRuntimeIo, reason: string) => Promise; readonly #commandDelivery: DriverCommandDelivery; #activeWorkFailure: { error: unknown } | null = null; - #activeInputCancellation: AbortController | null = null; + #activeRunTicket: DriverRunTicket | null = null; + readonly #activeMcpAdmissions = new Set(); readonly #activeMcpCommands = new Map(); + readonly #mcpRunFailures = new Map(); + readonly #terminalRunFences = new Set(); #activeRunGeneration = 0; #activeRunTask: Promise | null = null; #shutdownCompleted = false; @@ -120,12 +184,17 @@ export class DriverCommandDispatcher { async run(socket: DriverRuntimeIo, logger: Logger): Promise { const runtimeContext = this.#runtimeContextFactory(socket, logger); + const unregisterRunTerminalBarrier = socket.registerRunTerminalBarrier((events) => + this.#waitForRunTerminal(socket, events), + ); let joiningActiveWork = false; const onShutdown = () => { this.#shutdownPermissionTask = this.#permissionRequests.rejectAllAndWait(); void this.#shutdownPermissionTask.catch(() => {}); this.#abortActiveWork( + socket, toErrorMessage(this.#shutdownSignal.reason, "driver.command_loop.stopped"), + "shutdown", ); }; this.#shutdownSignal.addEventListener("abort", onShutdown, { once: true }); @@ -152,12 +221,15 @@ export class DriverCommandDispatcher { await logger.span("driver.command-loop", async () => { while (!this.#isShuttingDown()) { let command: RuntimeCommand | null; + let commandIdentity: unknown; try { - command = await raceWithAbort( + const candidate = await raceWithAbort( runtimeContext.ports.commandSource.nextCommand(this.#shutdownSignal), this.#shutdownSignal, ); + command = candidate === null ? null : parseRuntimeCommand(candidate); + commandIdentity = candidate; } catch (error) { if (this.#isShuttingDown()) { return; @@ -175,15 +247,15 @@ export class DriverCommandDispatcher { continue; } - await this.#handleCommand(runtimeContext, socket, command); + await this.#handleCommand(runtimeContext, socket, command, commandIdentity); if (this.#isShuttingDown()) { return; } } }); - if (this.#runtimeState.isShuttingDown()) { - this.#abortActiveWork("driver.command_loop.stopped"); + if (this.#shutdownSignal.aborted || this.#runtimeState.isShuttingDown()) { + this.#abortActiveWork(socket, "driver.command_loop.stopped", "shutdown"); joiningActiveWork = true; await this.#joinActiveWork(); joiningActiveWork = false; @@ -202,7 +274,9 @@ export class DriverCommandDispatcher { this.#isShuttingDown() || this.#runtimeState.isShuttingDown()); this.#abortActiveWork( + socket, shuttingDown ? "driver.command_loop.stopped" : "driver.command_loop.failed", + "shutdown", ); if (shuttingDown) { @@ -291,17 +365,207 @@ export class DriverCommandDispatcher { throw failure; } finally { this.#shutdownSignal.removeEventListener("abort", onShutdown); + unregisterRunTerminalBarrier(); + } + } + + #waitForRunTerminal( + socket: DriverRuntimeIo, + events: readonly DriverEventInput[], + ): Promise | void { + const terminal = this.#readRunTerminal(socket, events); + + if (terminal === null) { + return; } + + this.#terminalRunFences.add(terminal.runId); + const permissions = this.#permissionRequests.rejectRunAndWait(terminal.runId); + const hasWork = + [...this.#activeMcpAdmissions].some((admission) => admission.runId === terminal.runId) || + [...this.#activeMcpCommands.values()].some((command) => command.runId === terminal.runId); + + if (hasWork) { + return permissions === undefined + ? this.#joinRunMcpWork(terminal) + : Promise.all([permissions, this.#joinRunMcpWork(terminal)]).then(() => undefined); + } + + const failure = this.#mcpRunFailures.get(terminal.runId); + if (permissions === undefined) { + if (failure !== undefined && terminal.kind !== "run.failed") { + throw failure.error; + } + return; + } + return permissions.then(() => { + if (failure !== undefined && terminal.kind !== "run.failed") { + throw failure.error; + } + }); + } + + #readRunTerminal( + socket: DriverRuntimeIo, + events: readonly DriverEventInput[], + ): RunTerminalEvent | null { + let terminal: RunTerminalEvent | null = null; + let terminalIndex = -1; + + for (const [index, event] of events.entries()) { + if ( + event.kind !== "run.cancelled" && + event.kind !== "run.completed" && + event.kind !== "run.failed" + ) { + continue; + } + if (terminal !== null) { + throw new Error("Driver event batch cannot contain multiple run terminals."); + } + + const runId = event.runId === undefined ? socket.currentRunId() : event.runId; + if (runId === null) { + throw new Error("Driver run terminal requires an active run."); + } + + terminal = { kind: event.kind, runId: parseRunId(runId) }; + terminalIndex = index; + } + + if (terminalIndex >= 0 && terminalIndex !== events.length - 1) { + throw new Error("Driver run terminal must be the final event in its batch."); + } + + return terminal; + } + + #beginMcpAdmission(runId: RunId): ActiveMcpAdmission | null { + if (this.#terminalRunFences.has(runId)) { + return null; + } + + const settlement = Promise.withResolvers(); + const admission: ActiveMcpAdmission = { + runId, + settle: () => settlement.resolve(), + settled: settlement.promise, + }; + this.#activeMcpAdmissions.add(admission); + return admission; + } + + async #joinRunMcpWork(terminal: RunTerminalEvent): Promise { + while (true) { + const admissions = [...this.#activeMcpAdmissions] + .filter((admission) => admission.runId === terminal.runId) + .map((admission) => admission.settled); + const commands = [...this.#activeMcpCommands.values()] + .filter((command) => command.runId === terminal.runId) + .map((command) => command.task); + const work = [...admissions, ...commands]; + + if (work.length === 0) { + break; + } + + await promiseWithTimeout(Promise.allSettled(work), { + label: `Driver MCP commands for run ${terminal.runId}`, + timeoutMs: ACTIVE_MCP_COMMIT_GRACE_MS, + }); + } + + const failure = this.#mcpRunFailures.get(terminal.runId); + if (failure !== undefined && terminal.kind !== "run.failed") { + throw failure.error; + } + } + + #clearRunMcpState(socket: DriverRuntimeIo, runId: RunId): void { + if ( + socket.currentRunId() === runId || + [...this.#activeMcpAdmissions].some((admission) => admission.runId === runId) || + [...this.#activeMcpCommands.values()].some((command) => command.runId === runId) + ) { + return; + } + + this.#mcpRunFailures.delete(runId); + this.#terminalRunFences.delete(runId); } async #handleCommand( runtimeContext: AgentDriverContext, socket: DriverRuntimeIo, command: RuntimeCommand, + commandIdentity: unknown, ): Promise { const commandSummary = summarizeRuntimeCommand(command); + const replay = this.#commandDelivery.replay(command, commandIdentity); + const mcpAdmission = + command.kind === "mcp.execute" && replay === null + ? this.#beginMcpAdmission(parseRunId(command.runId)) + : undefined; + + try { + await this.#handleAdmittedCommand( + runtimeContext, + socket, + command, + commandIdentity, + commandSummary, + replay, + mcpAdmission !== null, + ); + } catch (error) { + if (mcpAdmission !== undefined && mcpAdmission !== null) { + this.#mcpRunFailures.set(mcpAdmission.runId, { error }); + this.#terminalRunFences.add(mcpAdmission.runId); + } + throw error; + } finally { + if (mcpAdmission !== undefined && mcpAdmission !== null) { + this.#activeMcpAdmissions.delete(mcpAdmission); + mcpAdmission.settle(); + } + } + } + + async #handleAdmittedCommand( + runtimeContext: AgentDriverContext, + socket: DriverRuntimeIo, + command: RuntimeCommand, + commandIdentity: unknown, + commandSummary: ReturnType, + replay: CommandReceipt | null, + mcpAdmissionGranted: boolean, + ): Promise { + const replayMode = + replay === null ? null : replay.tracked.terminal === undefined ? "active" : "terminal"; + + try { + if (command.kind === "mcp.execute" && replay === null && !mcpAdmissionGranted) { + throw new Error(`Run ${command.runId} is already publishing its terminal event.`); + } + if (replay === null && command.kind === "input.start" && this.#activeRunTask !== null) { + await settleInput(this.#activeRunTask); + } + this.#assertCommandRunOwnership(socket, command, replayMode); + } catch (error) { + await this.#commandDelivery.reject(runtimeContext, command, { + error: toCommandFailure(command, error), + status: "failed", + }); + runtimeContext.logger.warn("driver.runtime.command.run-rejected", { + commandId: command.commandId, + commandKind: command.kind, + message: toErrorMessage(error, "Driver command does not target the active run."), + }); + return; + } + runtimeContext.logger.debug("driver.runtime.command.received", commandSummary); - const receipt = this.#commandDelivery.receive(command); + const receipt = replay ?? this.#commandDelivery.receive(command, commandIdentity); if (receipt.replay) { if (receipt.tracked.terminal) { @@ -314,13 +578,19 @@ export class DriverCommandDispatcher { const eagerCancellation = command.kind === "turn.cancel" - ? this.#cancelActiveWork(runtimeContext, command.reason ?? "turn.cancelled") + ? this.#cancelActiveWork( + runtimeContext, + socket, + command.reason ?? "turn.cancelled", + "turn.cancel", + ) : null; void eagerCancellation?.catch(() => {}); await this.#commandDelivery.accept(runtimeContext, command, receipt.tracked); try { if (command.kind === "permission.resolve") { + this.#assertCommandRunOwnership(socket, command); this.#permissionRequests.resolve(command.requestId, command.decision); await this.#commandDelivery.finish(runtimeContext, command, { status: "completed", @@ -329,31 +599,18 @@ export class DriverCommandDispatcher { } if (command.kind === "input.start") { - if (this.#activeRunTask) { - await settleInput(this.#activeRunTask); - } - if (this.#activeRunTask) { - throw new Error("Driver run input is already in progress."); - } - if (this.#runtimeState.status() !== "ready") { - throw new Error(`Driver is not ready for input: ${this.#runtimeState.status()}.`); - } - this.#activeRunGeneration += 1; this.#runtimeState.beginRun(this.#activeRunGeneration); - const cancellation = new AbortController(); const runId = parseRunId(command.runId); - this.#activeInputCancellation = cancellation; - this.#commandDelivery.resetRunTerminal(); - socket.beginRun(runId); + const ticket = socket.beginRun(runId); + this.#activeRunTicket = ticket; let activeRunTask!: Promise; activeRunTask = this.#runInputTask( runtimeContext, socket, command, - cancellation, this.#activeRunGeneration, - runId, + ticket, ) .catch(async (error: unknown) => { this.#activeWorkFailure ??= { error }; @@ -372,7 +629,7 @@ export class DriverCommandDispatcher { .finally(() => { if (this.#activeRunTask === activeRunTask) { this.#activeRunTask = null; - this.#activeInputCancellation = null; + this.#activeRunTicket = null; } }); this.#activeRunTask = activeRunTask; @@ -380,6 +637,7 @@ export class DriverCommandDispatcher { } if (command.kind === "mcp.execute") { + this.#assertCommandRunOwnership(socket, command); this.#startMcpCommand(runtimeContext, socket, command); return; } @@ -397,7 +655,9 @@ export class DriverCommandDispatcher { this.#runtimeState.enter("stopping"); await this.#cancelActiveWork( runtimeContext, + socket, reason, + "session.stop", this.#permissionRequests.rejectAllAndWait(), ); @@ -408,7 +668,7 @@ export class DriverCommandDispatcher { commandId: command.commandId, reason, }); - await this.#commandDelivery.claimRunTerminal(socket, "completed"); + await deliverRunTerminal(socket, { status: "completed" }); runtimeContext.logger.debug("driver.runtime.run.completed", { commandId: command.commandId, reason, @@ -435,24 +695,74 @@ export class DriverCommandDispatcher { } } + #assertCommandRunOwnership( + socket: DriverRuntimeIo, + command: RuntimeCommand, + replay: "active" | "terminal" | null = null, + ): void { + if (command.kind === "session.stop") { + return; + } + + const runId = parseRunId(command.runId); + const currentRunId = socket.currentRunId(); + + if (command.kind === "input.start") { + if ( + replay === "terminal" + ? currentRunId !== null && currentRunId !== runId + : replay === "active" + ? currentRunId !== runId + : this.#activeRunTask !== null || currentRunId !== null + ) { + throw new Error(`Input command ${command.commandId} cannot replace the active run.`); + } + if (replay === null && this.#runtimeState.status() !== "ready") { + throw new Error(`Driver is not ready for input: ${this.#runtimeState.status()}.`); + } + return; + } + + if (currentRunId !== runId && !(replay === "terminal" && currentRunId === null)) { + throw new Error(`Command ${command.commandId} does not target the active run.`); + } + } + #abortMcpCommands(reason: string): void { for (const { controller } of this.#activeMcpCommands.values()) { controller.abort(new Error(reason)); } } - #abortActiveWork(reason: string): void { - this.#permissionRequests.rejectAll(); - this.#activeInputCancellation?.abort(new DriverTurnCancelledError(reason)); - this.#abortMcpCommands(reason); + #abortActiveWork( + socket: DriverRuntimeIo, + reason: string, + source: "session.stop" | "shutdown" | "turn.cancel", + ): "already_claimed" | "claimed" | "idle" | "terminal_selected" { + const ticket = this.#activeRunTicket; + const cancellation = + ticket === null || socket.runSnapshot(ticket.runId) === null + ? "idle" + : socket.claimRunCancellation(ticket, reason); + + if (source !== "turn.cancel" || cancellation !== "terminal_selected") { + this.#permissionRequests.rejectAll(); + this.#abortMcpCommands(reason); + } + if (cancellation === "idle") { + this.#activeRunTicket = null; + } + return cancellation; } async #cancelActiveWork( runtimeContext: AgentDriverContext, + socket: DriverRuntimeIo, reason: string, + source: "session.stop" | "shutdown" | "turn.cancel", permissionCancellation?: Promise, ): Promise { - this.#abortActiveWork(reason); + const cancellation = this.#abortActiveWork(socket, reason, source); let permissionFailure: { error: unknown } | null = null; if (permissionCancellation !== undefined) { @@ -463,14 +773,16 @@ export class DriverCommandDispatcher { } } - const backendCancellation = promiseWithTimeout( - this.#backend.cancelActiveTurn(runtimeContext, reason), - { - label: "Active driver turn cancellation", - timeoutMs: ACTIVE_TURN_CANCEL_GRACE_MS, - }, - ); - const results = await Promise.allSettled([backendCancellation, this.#joinActiveWork()]); + const tasks = [this.#joinActiveWork()]; + if (source !== "turn.cancel" || cancellation !== "terminal_selected") { + tasks.push( + promiseWithTimeout(this.#backend.cancelActiveTurn(runtimeContext, reason), { + label: "Active driver turn cancellation", + timeoutMs: ACTIVE_TURN_CANCEL_GRACE_MS, + }), + ); + } + const results = await Promise.allSettled(tasks); const failure = results.find((result) => result.status === "rejected"); if (permissionFailure !== null) { @@ -506,7 +818,7 @@ export class DriverCommandDispatcher { Promise.allSettled([...this.#activeMcpCommands.values()].map(({ task }) => task)), { label: "Active driver MCP commands", - timeoutMs: ACTIVE_INPUT_SETTLE_GRACE_MS, + timeoutMs: ACTIVE_MCP_COMMIT_GRACE_MS, }, ), ); @@ -532,10 +844,14 @@ export class DriverCommandDispatcher { throw new Error(`Driver has ${MAX_ACTIVE_MCP_COMMANDS} active MCP commands.`); } + const runId = parseRunId(command.runId); const controller = new AbortController(); const task = this.#runMcpCommand(runtimeContext, socket, command, controller) - .catch(async (error: unknown) => { + .catch((error: unknown) => { this.#activeWorkFailure ??= { error }; + if (!this.#mcpRunFailures.has(runId)) { + this.#mcpRunFailures.set(runId, { error }); + } this.#rememberRunFailure({ code: "driver.mcp_task_failed", details: { commandId: command.commandId }, @@ -546,19 +862,22 @@ export class DriverCommandDispatcher { commandId: command.commandId, driverInstanceId: this.#driverInstanceId, }); - await this.#shutdown(socket, "driver.mcp_task_failed").catch((shutdownError: unknown) => { - runtimeContext.logger.error("driver.runtime.shutdown.failed", shutdownError, { - commandId: command.commandId, - }); - }); + throw error; }) .finally(() => { if (this.#activeMcpCommands.get(command.commandId)?.task === task) { this.#activeMcpCommands.delete(command.commandId); } + this.#clearRunMcpState(socket, runId); + }); + this.#activeMcpCommands.set(command.commandId, { controller, runId, task }); + void task.catch(async () => { + await this.#shutdown(socket, "driver.mcp_task_failed").catch((shutdownError: unknown) => { + runtimeContext.logger.error("driver.runtime.shutdown.failed", shutdownError, { + commandId: command.commandId, + }); }); - this.#activeMcpCommands.set(command.commandId, { controller, task }); - void task; + }); } async #runMcpCommand( @@ -567,22 +886,13 @@ export class DriverCommandDispatcher { command: Extract, controller: AbortController, ): Promise { - let effectMayBeClaimed = false; let durableResult: McpExecuteCommandResult | null = null; const effectLedger = runtimeContext.ports.eventSink; - if ( - effectLedger.claimExternalToolEffect === undefined || - effectLedger.completeExternalToolEffect === undefined || - effectLedger.markExternalToolEffectUnknown === undefined - ) { - throw new Error("Driver external tool effect ledger is not configured."); - } - const completeExternalToolEffect = effectLedger.completeExternalToolEffect.bind(effectLedger); - try { await pushLosslessEvents(socket, [ { + correlationId: command.commandId, kind: "tool.call.updated", payload: { kind: "mcp", @@ -591,51 +901,33 @@ export class DriverCommandDispatcher { title: command.toolName, toolCallId: command.toolCallId, }, + runId: parseRunId(command.runId), + sourceEventId: `mcp.execute.running:${command.commandId}`, }, ]); controller.signal.throwIfAborted(); - await using prepared = await runtimeContext.ports.mcp.prepare(command, controller.signal); - controller.signal.throwIfAborted(); - const pendingClaim = effectLedger.claimExternalToolEffect( + const observeExternalToolEffect = effectLedger.observeExternalToolEffect; + if ( + observeExternalToolEffect === undefined || + effectLedger.claimExternalToolEffect === undefined || + effectLedger.settleExternalToolEffect === undefined + ) { + throw new Error("Driver external tool effect ledger is not configured."); + } + const observed = await observeExternalToolEffect.call( + effectLedger, { commandId: command.commandId }, controller.signal, ); - effectMayBeClaimed = true; - const claim = await pendingClaim; - if (claim.kind === "unknown") { - effectMayBeClaimed = false; - throw new ExternalToolEffectResolutionRequiredError(command, claim.effectId); - } - - let result: McpExecuteCommandResult; - if (claim.kind === "completed") { - effectMayBeClaimed = false; - result = claim.result; - durableResult = result; - } else { - runtimeContext.logger.info("driver.runtime.mcp.execute.started", { - effectAttempt: claim.attempt, - serverId: command.serverId, - toolName: command.toolName, - }); - const execution = await prepared.execute(claim); - const { providerReceiptJson, ...executionResult } = execution; - result = executionResult; - const complete = () => - completeExternalToolEffect( - { - commandId: command.commandId, - ...(providerReceiptJson === undefined ? {} : { providerReceiptJson }), - result, - }, - AbortSignal.timeout(EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS), - ); - await Promise.resolve().then(complete).catch(complete); - durableResult = result; - } + const result = + observed.kind === "intent" + ? await this.#executeMcpIntent(runtimeContext, command, controller, observed.effectId) + : this.#resolveExternalToolEffectState(command, observed, observed.effectId); + durableResult = result; await pushLosslessEvents(socket, [ { + correlationId: command.commandId, kind: "tool.call.updated", payload: { kind: "mcp", @@ -645,6 +937,8 @@ export class DriverCommandDispatcher { title: command.toolName, toolCallId: command.toolCallId, }, + runId: parseRunId(command.runId), + sourceEventId: `mcp.execute.completed:${command.commandId}`, }, ]); await this.#commandDelivery.finish(runtimeContext, command, { @@ -657,17 +951,11 @@ export class DriverCommandDispatcher { toolName: command.toolName, }); } catch (error) { - if (effectMayBeClaimed && durableResult === null) { - await this.#markExternalToolEffectUnknown(effectLedger, command.commandId); + if (durableResult !== null) { + throw error; } - if (durableResult !== null) { - if (!this.#commandDelivery.hasTerminal(command.commandId)) { - await this.#commandDelivery.finish(runtimeContext, command, { - result: durableResult, - status: "completed", - }); - } + if (error instanceof ExternalToolEffectUnsettledError) { throw error; } @@ -675,47 +963,215 @@ export class DriverCommandDispatcher { throw error; } - if (controller.signal.aborted) { + if (controller.signal.aborted && !(error instanceof ExternalToolEffectUnknownError)) { + await pushLosslessEvents(socket, [ + { + correlationId: command.commandId, + kind: "tool.call.updated", + payload: { + kind: "mcp", + rawInput: command.argumentsJson, + status: "cancelled", + title: command.toolName, + toolCallId: command.toolCallId, + }, + runId: parseRunId(command.runId), + sourceEventId: `mcp.execute.cancelled:${command.commandId}`, + }, + ]); await this.#commandDelivery.finish(runtimeContext, command, { status: "cancelled", }); return; } + const commandFailure = toCommandFailure(command, error); + const failedPayload = { + kind: "mcp", + rawInput: command.argumentsJson, + rawOutput: commandFailure.message, + status: "failed", + title: command.toolName, + toolCallId: command.toolCallId, + } as const; + const failedSourceEventId = `mcp.execute.failed:${createHash("sha256") + .update(JSON.stringify([command.commandId, failedPayload])) + .digest("hex")}`; + await pushLosslessEvents(socket, [ { + correlationId: command.commandId, kind: "tool.call.updated", - payload: { - kind: "mcp", - rawInput: command.argumentsJson, - rawOutput: toErrorMessage(error, "MCP tool execution failed."), - status: "failed", - title: command.toolName, - toolCallId: command.toolCallId, - }, + payload: failedPayload, + runId: parseRunId(command.runId), + sourceEventId: failedSourceEventId, }, - ]).catch((deliveryError: unknown) => { - runtimeContext.logger.error("driver.runtime.mcp.failed-event.failed", deliveryError, { + ]); + + await this.#failCommand(runtimeContext, socket, command, error, commandFailure); + } + } + + async #executeMcpIntent( + runtimeContext: AgentDriverContext, + command: Extract, + controller: AbortController, + effectId: string, + ): Promise { + this.#assertActiveMcpRun(runtimeContext, command); + const durableResultIdentity = requireDurableMcpResultIdentity(command); + const prepared = await runtimeContext.ports.mcp.prepare(command, controller.signal); + + try { + controller.signal.throwIfAborted(); + this.#assertActiveMcpRun(runtimeContext, command); + const claimToken = randomUUID(); + const claimInput = { claimToken, commandId: command.commandId } as const; + const claimExternalToolEffect = runtimeContext.ports.eventSink.claimExternalToolEffect; + let claim: McpExternalToolEffectState; + + if (claimExternalToolEffect === undefined) { + throw new Error("Driver external tool effect ledger is not configured."); + } + + try { + claim = await retryOnce(() => + claimExternalToolEffect.call( + runtimeContext.ports.eventSink, + claimInput, + AbortSignal.timeout(EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS), + ), + ); + } catch (error) { + runtimeContext.logger.warn("driver.runtime.mcp.claim.outcome-unknown", { + commandId: command.commandId, + effectId, + message: toErrorMessage(error, "External effect claim failed."), + }); + throw new ExternalToolEffectUnsettledError(command, effectId, error); + } + + if (claim.kind !== "claimed") { + return this.#resolveExternalToolEffectState(command, claim, effectId); + } + if (claim.effectId !== effectId) { + throw new ExternalToolEffectUnsettledError( + command, + effectId, + new Error(`External effect claim returned mismatched effect ID ${claim.effectId}.`), + ); + } + + runtimeContext.logger.info("driver.runtime.mcp.execute.started", { + effectAttempt: claim.attempt, + serverId: command.serverId, + toolName: command.toolName, + }); + let execution: Awaited>; + + try { + execution = await prepared.execute(claim); + } catch (error) { + runtimeContext.logger.warn("driver.runtime.mcp.execute.outcome-unknown", { commandId: command.commandId, - toolCallId: command.toolCallId, + effectId, + message: toErrorMessage(error, "MCP execution outcome is unknown."), + }); + const settled = await this.#settleExternalToolEffect( + runtimeContext, + command, + claimToken, + effectId, + { kind: "unknown" }, + ); + return this.#resolveExternalToolEffectState(command, settled, effectId); + } + + const settled = await this.#settleExternalToolEffect( + runtimeContext, + command, + claimToken, + effectId, + createDurableMcpSucceededSettlement(execution, durableResultIdentity), + ); + return this.#resolveExternalToolEffectState(command, settled, effectId); + } finally { + await Promise.resolve(prepared[Symbol.asyncDispose]()).catch((error: unknown) => { + runtimeContext.logger.warn("driver.runtime.mcp.cleanup.failed", { + commandId: command.commandId, + message: toErrorMessage(error, "MCP cleanup failed."), }); }); + } + } - await this.#failCommand(runtimeContext, socket, command, error); + #assertActiveMcpRun( + runtimeContext: AgentDriverContext, + command: Extract, + ): void { + if (runtimeContext.ports.eventSink.currentRunId() !== parseRunId(command.runId)) { + throw new Error(`Command ${command.commandId} does not target the active run.`); } } - async #markExternalToolEffectUnknown( - effectLedger: AgentDriverContext["ports"]["eventSink"], - commandId: string, - ): Promise { - const markUnknown = effectLedger.markExternalToolEffectUnknown; - if (markUnknown === undefined) { + #resolveExternalToolEffectState( + command: Extract, + state: McpExternalToolEffectState, + effectId: string, + ): McpExecuteCommandResult { + if (state.effectId !== effectId) { + throw new ExternalToolEffectUnsettledError( + command, + effectId, + new Error(`External effect state returned mismatched effect ID ${state.effectId}.`), + ); + } + if (state.kind === "succeeded") { + if ( + state.result.requestId !== command.requestId || + state.result.serverId !== command.serverId || + state.result.toolName !== command.toolName + ) { + throw new ExternalToolEffectUnsettledError( + command, + effectId, + new Error("External effect result does not match the current MCP command."), + ); + } + return state.result; + } + if (state.kind === "unknown") { + throw new ExternalToolEffectUnknownError(command, effectId); + } + + throw new ExternalToolEffectUnsettledError(command, effectId); + } + + async #settleExternalToolEffect( + runtimeContext: AgentDriverContext, + command: Extract, + claimToken: string, + effectId: string, + settlement: McpExternalToolEffectSettlement, + ): Promise { + const input = { claimToken, commandId: command.commandId, effectId, settlement } as const; + const settleExternalToolEffect = runtimeContext.ports.eventSink.settleExternalToolEffect; + + if (settleExternalToolEffect === undefined) { throw new Error("Driver external tool effect ledger is not configured."); } - const signal = AbortSignal.timeout(EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS); - await markUnknown.call(effectLedger, { commandId }, signal); + try { + return await retryOnce(() => + settleExternalToolEffect.call( + runtimeContext.ports.eventSink, + input, + AbortSignal.timeout(EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS), + ), + ); + } catch (error) { + throw new ExternalToolEffectUnsettledError(command, effectId, error); + } } async #failCommand( @@ -723,9 +1179,8 @@ export class DriverCommandDispatcher { socket: DriverRuntimeIo, command: RuntimeCommand, error: unknown, + commandFailure = toCommandFailure(command, error), ): Promise { - const commandFailure = toCommandFailure(command, error); - if (command.kind === "session.stop") { if (this.#runtimeState.status() === "stopping") { this.#runtimeState.enter("failed"); @@ -780,109 +1235,96 @@ export class DriverCommandDispatcher { runtimeContext: AgentDriverContext, socket: DriverRuntimeIo, command: Extract, - cancellation: AbortController, - runId: RunId, - ): Promise { - let terminalSelectedAtCancellation: "cancelled" | "completed" | "failed" | null = null; - const captureTerminalSelection = () => { - terminalSelectedAtCancellation = - socket.selectedRunEventTerminal?.(runId) ?? socket.runEventTerminal?.(runId) ?? null; - }; - cancellation.signal.addEventListener("abort", captureTerminalSelection, { once: true }); - if (cancellation.signal.aborted) { - captureTerminalSelection(); + ticket: DriverRunTicket, + ): Promise<"command_acked" | "driver_failing"> { + let outcome: DriverInputOutcome; + try { + ticket.signal.throwIfAborted(); + await this.#backend.handleInput( + runtimeContext, + { text: command.input.text }, + ticket.runId, + ticket.signal, + ); + outcome = { status: "resolved" }; + } catch (error) { + outcome = isDriverTurnCancelledError(error) + ? { error, status: "cancelled" } + : { error, status: "rejected" }; } - try { - cancellation.signal.throwIfAborted(); - await this.#backend.handleInput(runtimeContext, command.input, runId, cancellation.signal); - if (cancellation.signal.aborted) { - const deliveredTerminal = socket.runEventTerminal?.(runId) ?? null; - if ( - terminalSelectedAtCancellation === null || - deliveredTerminal !== terminalSelectedAtCancellation - ) { - cancellation.signal.throwIfAborted(); - } - } + const settlement = socket.settleRunInput(ticket, outcome); + if (settlement.status === "resolved") { await this.#commandDelivery.finish(runtimeContext, command, { result: { requestId: command.requestId, }, status: "completed", }); - } catch (error) { - if (this.#commandDelivery.hasTerminal(command.commandId)) { - throw error; - } - - if ( - isDriverTurnCancelledError(error) || - (cancellation.signal.aborted && - !(error instanceof DriverTurnCancellationCleanupError) && - !(error instanceof PermissionEventDeliveryError)) - ) { - await this.#commandDelivery.finish(runtimeContext, command, { - status: "cancelled", - }); - runtimeContext.logger.info("driver.runtime.input.cancelled", { - commandId: command.commandId, - commandKind: command.kind, - driverInstanceId: this.#driverInstanceId, - }); - - return; - } - - const commandFailure = toCommandFailure(command, error); - - this.#runtimeState.enter("failed"); + return "command_acked"; + } + if (settlement.status === "cancelled") { await this.#commandDelivery.finish(runtimeContext, command, { - error: commandFailure, - status: "failed", + status: "cancelled", }); - if (socket.runEventTerminal?.(runId) == null) { - await pushDriverDiagnosticEvent( - socket, - { - code: "driver.command_failed", - details: { - commandId: command.commandId, - commandKind: command.kind, - }, - message: commandFailure.message, - severity: "error", - source: "core", - }, - runtimeContext.logger, - ); - } - runtimeContext.logger.error("driver.runtime.command.failed", error, { + runtimeContext.logger.info("driver.runtime.input.cancelled", { commandId: command.commandId, commandKind: command.kind, driverInstanceId: this.#driverInstanceId, - fatal: true, }); - this.#rememberRunFailure(commandFailure); - await this.#shutdown(socket, commandFailure.code); - this.#shutdownCompleted = true; - } finally { - cancellation.signal.removeEventListener("abort", captureTerminalSelection); + return "command_acked"; } + + const commandFailure = toCommandFailure(command, settlement.failure); + this.#runtimeState.enter("failed"); + await this.#commandDelivery.finish(runtimeContext, command, { + error: commandFailure, + status: "failed", + }); + if (socket.runSnapshot(ticket.runId)?.terminal === null) { + await pushDriverDiagnosticEvent( + socket, + { + code: "driver.command_failed", + details: { + commandId: command.commandId, + commandKind: command.kind, + }, + message: commandFailure.message, + severity: "error", + source: "core", + }, + runtimeContext.logger, + ); + } + runtimeContext.logger.error("driver.runtime.command.failed", settlement.failure, { + commandId: command.commandId, + commandKind: command.kind, + driverInstanceId: this.#driverInstanceId, + fatal: true, + }); + this.#rememberRunFailure(commandFailure); + await this.#shutdown(socket, commandFailure.code); + this.#shutdownCompleted = true; + return "driver_failing"; } async #runInputTask( runtimeContext: AgentDriverContext, socket: DriverRuntimeIo, command: Extract, - cancellation: AbortController, generation: number, - runId: RunId, + ticket: DriverRunTicket, ): Promise { + let releaseReason: "command_acked" | "driver_failing" = "driver_failing"; try { - await this.#runInputCommand(runtimeContext, socket, command, cancellation, runId); + releaseReason = await this.#runInputCommand(runtimeContext, socket, command, ticket); } finally { - socket.endRun(runId); + socket.releaseRun(ticket, releaseReason); + this.#clearRunMcpState(socket, ticket.runId); + if (this.#activeRunTicket === ticket) { + this.#activeRunTicket = null; + } this.#runtimeState.endRun(generation); } } diff --git a/src/core/driver-permission-broker.ts b/src/core/driver-permission-broker.ts index bca55c2..f564e27 100644 --- a/src/core/driver-permission-broker.ts +++ b/src/core/driver-permission-broker.ts @@ -1,3 +1,5 @@ +import { createHash } from "node:crypto"; + import { summarizeDriverPermissionRequest } from "../observability/driver-debug"; import type { Logger } from "../observability"; import type { DriverPermissionRequest } from "../host-ports"; @@ -5,7 +7,7 @@ import type { DriverEventInput } from "../protocol/events"; import type { RunId } from "../protocol/id"; import { promiseWithTimeout, settlePromiseWithTimeout } from "../utils/async"; import { createDriverDiagnosticEvent } from "./driver-diagnostics"; -import { pushLosslessEvents } from "./driver-runtime-io"; +import { DriverEventDeliveryOutcomeUnknownError, pushLosslessEvents } from "./driver-runtime-io"; import type { DriverRuntimeEventPort } from "./driver-runtime-io"; const PERMISSION_REQUEST_TIMEOUT_MS = 5 * 60 * 1000; @@ -41,6 +43,11 @@ interface PermissionCancellationDelivery { useTurnBudget(): void; } +interface PermissionDeliveryFailure { + readonly error: PermissionEventDeliveryError; + recover(): Promise; +} + export interface DriverPermissionBrokerOptions { eventDeliveryTimeoutMs?: number; interactiveRequests?: boolean; @@ -60,6 +67,8 @@ export class DriverPermissionBroker { #activeRequestBytes = 0; readonly #cancellationDeliveries = new Map(); #cancellationTask: Promise | null = null; + #closedRunId: RunId | null = null; + readonly #deliveryFailures = new Map(); #idle: PromiseWithResolvers | null = null; readonly #resolvers = new Map void>(); @@ -134,7 +143,7 @@ export class DriverPermissionBroker { this.#rejectAll(reason); if (!this.hasPending()) { - return Promise.resolve(); + return this.#recoverDeliveryFailures(); } if (this.#cancellationTask !== null) { return this.#cancellationTask; @@ -144,7 +153,7 @@ export class DriverPermissionBroker { const task = promiseWithTimeout(idle.promise, { label: "Driver permission cancellation", timeoutMs: this.#eventDeliveryTimeoutMs * 2, - }); + }).then(() => this.#recoverDeliveryFailures()); this.#cancellationTask = task; const clear = () => { if (this.#cancellationTask === task) { @@ -155,6 +164,51 @@ export class DriverPermissionBroker { return task; } + rejectRunAndWait( + runId: RunId, + reason: PermissionResolutionReason = "cancelled", + ): Promise | void { + this.#closedRunId = runId; + if (!this.hasPending() && this.#deliveryFailures.size === 0) { + this.#rejectAll(reason); + return; + } + return this.rejectAllAndWait(reason); + } + + async #recoverDeliveryFailures(): Promise { + const failures = [...this.#deliveryFailures.entries()]; + await Promise.allSettled( + failures.map(async ([lifecycleId, failure]) => { + const recovery = failure.recover(); + const result = await settlePromiseWithTimeout(recovery, { + label: "Driver permission lifecycle recovery", + timeoutMs: this.#eventDeliveryTimeoutMs, + }); + if (result.status !== "completed") { + if (result.status === "timed_out") { + void recovery.then( + () => { + if (this.#deliveryFailures.get(lifecycleId) === failure) { + this.#deliveryFailures.delete(lifecycleId); + } + }, + () => {}, + ); + } + throw result.error; + } + if (this.#deliveryFailures.get(lifecycleId) === failure) { + this.#deliveryFailures.delete(lifecycleId); + } + }), + ); + const failure = this.#deliveryFailures.values().next().value; + if (failure !== undefined) { + throw failure.error; + } + } + private resolveRequest(requestId: string, resolution: PermissionResolution): boolean { const resolve = this.#resolvers.get(requestId); @@ -181,6 +235,12 @@ export class DriverPermissionBroker { return "reject_once"; } + const runId = socket.currentRunId(); + const isCurrentRun = ownsRun ?? (() => socket.currentRunId() === runId); + if (!isCurrentRun() || (runId !== null && this.#closedRunId === runId)) { + return "reject_once"; + } + if (this.#activeRequestIds.has(input.requestId)) { throw new Error(`Driver permission request ${input.requestId} is already pending.`); } @@ -195,8 +255,7 @@ export class DriverPermissionBroker { throw new RangeError("Driver permission broker pending request byte budget is exhausted."); } - const runId = socket.currentRunId(); - const isCurrentRun = ownsRun ?? (() => socket.currentRunId() === runId); + const lifecycleId = permissionLifecycleId(runId, input.requestId); const events: DriverEventInput[] = [ { kind: "permission.requested", @@ -217,6 +276,7 @@ export class DriverPermissionBroker { }, }, ...(runId === null ? {} : { runId }), + sourceEventId: `${lifecycleId}:requested`, }, ]; @@ -305,36 +365,76 @@ export class DriverPermissionBroker { timeoutMs: this.#requestTimeoutMs, }); - const requestedTask = pushLosslessEvents( - socket, - events, - undefined, + const deliverySignal = () => AbortSignal.any([ cancellationDelivery.signal, AbortSignal.timeout(this.#eventDeliveryTimeoutMs), - ]), - ); + ]); + const requestedTask = pushPermissionEvents(socket, events, deliverySignal()); const requestedDelivery = await settlePromiseWithTimeout(requestedTask, { label: `Driver permission request ${input.requestId} event delivery`, timeoutMs: this.#eventDeliveryTimeoutMs, }); if (requestedDelivery.status !== "completed") { - if (requestedDelivery.status === "timed_out") { - lateDelivery = requestedTask; - } - if (!isCurrentRun()) { - return "reject_once"; - } - throw new PermissionEventDeliveryError( + const deliveryError = new PermissionEventDeliveryError( input.requestId, "requested", requestedDelivery.error, ); - } + const outcomeUnknown = + requestedDelivery.status === "timed_out" || + requestedDelivery.error instanceof DriverEventDeliveryOutcomeUnknownError; - if (!isCurrentRun()) { - return "reject_once"; + if (!outcomeUnknown) { + throw deliveryError; + } + + const cancelled: PermissionResolution = { + decision: "reject_once", + reason: "cancelled", + }; + const resolutionEvents = toResolutionEvents(input, cancelled, runId, lifecycleId); + this.#deliveryFailures.set(lifecycleId, { + error: deliveryError, + recover: async () => { + await pushPermissionEvents( + socket, + [...events, ...resolutionEvents], + AbortSignal.timeout(this.#eventDeliveryTimeoutMs), + ); + }, + }); + const closeLifecycle = () => + pushPermissionEvents(socket, [...events, ...resolutionEvents], deliverySignal()).then( + () => { + this.#deliveryFailures.delete(lifecycleId); + }, + ); + + if (requestedDelivery.status === "timed_out") { + lateDelivery = requestedTask.then( + () => pushPermissionEvents(socket, resolutionEvents, deliverySignal()), + closeLifecycle, + ); + lateDelivery = lateDelivery.then(() => { + this.#deliveryFailures.delete(lifecycleId); + }); + throw deliveryError; + } + + const recoveryTask = closeLifecycle(); + const recovery = await settlePromiseWithTimeout(recoveryTask, { + label: `Driver permission request ${input.requestId} lifecycle recovery`, + timeoutMs: this.#eventDeliveryTimeoutMs, + }); + if (recovery.status === "completed") { + return "reject_once"; + } + if (recovery.status === "timed_out") { + lateDelivery = recoveryTask; + } + throw new PermissionEventDeliveryError(input.requestId, "requested", recovery.error); } this.#logger()?.debug("driver.runtime.permission.request.sent", { @@ -344,19 +444,27 @@ export class DriverPermissionBroker { toolKind: input.toolKind, }); - const result = await settlePromiseWithTimeout(deferred.promise, { - label: `Driver permission request ${input.requestId}`, - timeoutMs: this.#requestTimeoutMs, - }); + const result = isCurrentRun() + ? await settlePromiseWithTimeout(deferred.promise, { + label: `Driver permission request ${input.requestId}`, + timeoutMs: this.#requestTimeoutMs, + }) + : ({ + status: "completed", + value: { decision: "reject_once", reason: "cancelled" }, + } as const); if (result.status === "failed") { throw result.error; } - const resolution: PermissionResolution = + let resolution: PermissionResolution = result.status === "timed_out" ? { decision: "reject_once", reason: "timed_out" } : result.value; + if (!isCurrentRun()) { + resolution = { decision: "reject_once", reason: "cancelled" }; + } unregister(); const decision = resolution.decision; @@ -367,50 +475,41 @@ export class DriverPermissionBroker { }); } - if (!isCurrentRun()) { - return "reject_once"; - } - - const resolutionTask = pushLosslessEvents( - socket, - [ - { - kind: "permission.resolved", - payload: { - outcome: decision, - permissionRequests: [], - reason: resolution.reason, - requestId: input.requestId, - }, - ...(runId === null ? {} : { runId }), - }, - ...toResolutionDiagnostics(input, resolution.reason, runId), - ], - undefined, - AbortSignal.any([ - cancellationDelivery.signal, - AbortSignal.timeout(this.#eventDeliveryTimeoutMs), - ]), - ); + const resolutionEvents = toResolutionEvents(input, resolution, runId, lifecycleId); + const resolutionTask = pushPermissionEvents(socket, resolutionEvents, deliverySignal()); const resolutionDelivery = await settlePromiseWithTimeout(resolutionTask, { label: `Driver permission resolution ${input.requestId} event delivery`, timeoutMs: this.#eventDeliveryTimeoutMs, }); if (resolutionDelivery.status !== "completed") { - if (resolutionDelivery.status === "timed_out") { - lateDelivery = resolutionTask; - } - if (!isCurrentRun()) { - return "reject_once"; - } - throw new PermissionEventDeliveryError( + const deliveryError = new PermissionEventDeliveryError( input.requestId, "resolved", resolutionDelivery.error, ); + this.#deliveryFailures.set(lifecycleId, { + error: deliveryError, + recover: async () => { + await pushPermissionEvents( + socket, + resolutionEvents, + AbortSignal.timeout(this.#eventDeliveryTimeoutMs), + ); + }, + }); + if (resolutionDelivery.status === "timed_out") { + lateDelivery = resolutionTask + .catch(() => pushPermissionEvents(socket, resolutionEvents, deliverySignal())) + .then(() => { + this.#deliveryFailures.delete(lifecycleId); + }); + } + throw deliveryError; } + this.#deliveryFailures.delete(lifecycleId); + this.#logger()?.debug("driver.runtime.permission.request.resolved", { decision, reason: resolution.reason, @@ -433,10 +532,62 @@ export class DriverPermissionBroker { } } +function permissionLifecycleId(runId: RunId | null, requestId: string): string { + const digest = createHash("sha256") + .update(JSON.stringify([runId, requestId])) + .digest("hex"); + return `permission:${digest}`; +} + +async function pushPermissionEvents( + socket: Pick, + events: readonly DriverEventInput[], + signal: AbortSignal, +) { + try { + return await pushLosslessEvents(socket, events, undefined, signal); + } catch (error) { + if (!(error instanceof DriverEventDeliveryOutcomeUnknownError)) { + throw error; + } + + try { + return await pushLosslessEvents(socket, events, undefined, signal); + } catch (retryError) { + throw retryError instanceof DriverEventDeliveryOutcomeUnknownError + ? retryError + : new DriverEventDeliveryOutcomeUnknownError(retryError); + } + } +} + +function toResolutionEvents( + input: DriverPermissionRequest, + resolution: PermissionResolution, + runId: RunId | null, + lifecycleId: string, +): DriverEventInput[] { + return [ + { + kind: "permission.resolved", + payload: { + outcome: resolution.decision, + permissionRequests: [], + reason: resolution.reason, + requestId: input.requestId, + }, + ...(runId === null ? {} : { runId }), + sourceEventId: `${lifecycleId}:resolved`, + }, + ...toResolutionDiagnostics(input, resolution.reason, runId, lifecycleId), + ]; +} + function toResolutionDiagnostics( input: DriverPermissionRequest, reason: PermissionResolutionReason, runId: RunId | null, + lifecycleId: string, ): DriverEventInput[] { if (reason !== "cancelled" && reason !== "timed_out") { return []; @@ -460,6 +611,7 @@ function toResolutionDiagnostics( source: "permission", }), ...(runId === null ? {} : { runId }), + sourceEventId: `${lifecycleId}:diagnostic:${reason}`, }, ]; } diff --git a/src/core/driver-runtime-io.ts b/src/core/driver-runtime-io.ts index f698fdd..2175213 100644 --- a/src/core/driver-runtime-io.ts +++ b/src/core/driver-runtime-io.ts @@ -9,12 +9,18 @@ import type { DriverHeartbeatOutput, } from "../protocol/orpc"; import type { - McpExecuteCommandResult, + DriverCommandUpdate, McpExternalToolEffectClaim, - RunError, + McpExternalToolEffectSettlement, + McpExternalToolEffectState, RuntimeCommand, - RuntimeCommandResult, } from "../runtime-command"; +import type { + DriverInputOutcome, + DriverInputSettlement, + DriverRunSnapshot, + DriverRunTicket, +} from "./driver-terminal-state"; export interface DriverRuntimeEventPort { currentRunId(): RunId | null; @@ -22,10 +28,12 @@ export interface DriverRuntimeEventPort { events: DriverEventInput[]; signal?: AbortSignal; }): Promise; - runEventTerminal?(runId: RunId): "cancelled" | "completed" | "failed" | null; - selectedRunEventTerminal?(runId: RunId): "cancelled" | "completed" | "failed" | null; } +export type DriverRunTerminalBarrier = ( + events: readonly DriverEventInput[], +) => Promise | void; + export const DRIVER_EVENT_DELIVERY_TIMEOUT_MS = 10_000; export class DriverEventRejectedError extends Error { @@ -38,6 +46,15 @@ export class DriverEventRejectedError extends Error { } } +export class DriverEventDeliveryOutcomeUnknownError extends Error { + constructor(cause: unknown) { + super(cause instanceof Error ? cause.message : "Driver event delivery outcome is unknown.", { + cause, + }); + this.name = "DriverEventDeliveryOutcomeUnknownError"; + } +} + /** * Stamps missing source IDs without taking ownership of event payloads. * @@ -51,6 +68,19 @@ export function withSourceEventIds(events: readonly DriverEventInput[]): DriverE ); } +export function assertIsolatedRunTerminalBatch(events: readonly DriverEventInput[]): void { + const terminals = events.filter( + ({ kind }) => kind === "run.cancelled" || kind === "run.completed" || kind === "run.failed", + ); + + if (terminals.length > 1) { + throw new Error("Driver event batch cannot contain multiple run terminals."); + } + if (terminals.length === 1 && events.length !== 1) { + throw new Error("Driver run terminal must be the only event in its batch."); + } +} + export function assertDriverEventReceiptPrefix( events: readonly DriverEventInput[], receipts: readonly DriverEventReceipt[], @@ -75,7 +105,7 @@ export function assertDriverEventReceiptPrefix( const event = events[index]; const eventId = event?.sourceEventId ?? event?.id; - if (receipt.eventId !== undefined && receipt.eventId !== eventId) { + if (receipt.eventId !== eventId) { throw new Error(`Driver event receipt ${index} does not match the submitted event ID.`); } } @@ -107,10 +137,14 @@ export async function pushLosslessEvents( signal: deadline, }); } catch (error) { - if (error instanceof DriverEventRejectedError || !retryTransport) { + if (error instanceof DriverEventRejectedError) { throw error; } + if (!retryTransport) { + throw new DriverEventDeliveryOutcomeUnknownError(error); + } + retryTransport = false; continue; } @@ -131,40 +165,42 @@ export async function pushLosslessEvents( } export interface DriverRuntimeCommandPort { - commandUpdate( - input: { - commandId: string; - error?: RunError; - result?: RuntimeCommandResult; - status: "accepted" | "cancelled" | "completed" | "failed"; - }, - signal: AbortSignal, - ): Promise; + commandUpdate(input: DriverCommandUpdate, signal: AbortSignal): Promise; nextCommand(signal: AbortSignal): Promise; } /** API-owned durable effect ledger used only for external MCP calls. */ export interface DriverRuntimeExternalToolEffectPort { claimExternalToolEffect( - input: { commandId: string }, + input: { claimToken: string; commandId: string }, signal: AbortSignal, ): Promise; - completeExternalToolEffect( + observeExternalToolEffect( + input: { commandId: string }, + signal: AbortSignal, + ): Promise; + settleExternalToolEffect( input: { + claimToken: string; commandId: string; - providerReceiptJson?: string | null | undefined; - result: McpExecuteCommandResult; + effectId: string; + settlement: McpExternalToolEffectSettlement; }, signal: AbortSignal, - ): Promise; - markExternalToolEffectUnknown(input: { commandId: string }, signal: AbortSignal): Promise; + ): Promise; } export interface DriverRuntimeRunPort { - beginRun(runId: RunId): void; + beginRun(runId: RunId): DriverRunTicket; + claimRunCancellation( + ticket: DriverRunTicket, + reason: string, + ): "already_claimed" | "claimed" | "terminal_selected"; completeRun(signal?: AbortSignal): Promise; - endRun(runId: RunId): void; failRun(error: DriverFailureInput["error"], signal?: AbortSignal): Promise; + releaseRun(ticket: DriverRunTicket, reason: "command_acked" | "driver_failing"): void; + runSnapshot(runId?: RunId): DriverRunSnapshot | null; + settleRunInput(ticket: DriverRunTicket, outcome: DriverInputOutcome): DriverInputSettlement; } export interface DriverRuntimeHeartbeatPort { @@ -177,4 +213,6 @@ export interface DriverRuntimeIo DriverRuntimeExternalToolEffectPort, DriverRuntimeEventPort, DriverRuntimeHeartbeatPort, - DriverRuntimeRunPort {} + DriverRuntimeRunPort { + registerRunTerminalBarrier(barrier: DriverRunTerminalBarrier): () => void; +} diff --git a/src/core/driver-terminal-state.ts b/src/core/driver-terminal-state.ts new file mode 100644 index 0000000..b8a9c7d --- /dev/null +++ b/src/core/driver-terminal-state.ts @@ -0,0 +1,356 @@ +import { isDeepStrictEqual } from "node:util"; + +import type { DriverEventInput } from "../protocol/events"; +import type { RunId } from "../protocol/id"; +import type { DriverEventReceipt } from "../protocol/orpc"; +import type { RunError } from "../runtime-command"; +import { DriverTurnCancelledError } from "./driver-turn-cancelled-error"; + +export type DriverRunTerminalStatus = "cancelled" | "completed" | "failed"; + +export interface DriverRunTicket { + readonly revision: number; + readonly runId: RunId; + readonly signal: AbortSignal; +} + +export interface DriverRunTerminalIdentity { + readonly event: DriverEventInput; + readonly runId: RunId; + readonly sourceEventId: string; + readonly status: DriverRunTerminalStatus; +} + +export type DriverRunTerminalState = + | { + readonly phase: "selected"; + readonly value: DriverRunTerminalIdentity; + } + | { + readonly phase: "acked"; + readonly receipt: DriverEventReceipt; + readonly value: DriverRunTerminalIdentity; + }; + +export interface DriverRunSnapshot { + readonly cancellation: { + readonly reason: string; + } | null; + readonly revision: number; + readonly runId: RunId; + readonly terminal: DriverRunTerminalState | null; +} + +export type DriverInstanceTerminal = + | { readonly runId: RunId; readonly status: "completed" } + | { readonly error: RunError; readonly runId: RunId; readonly status: "failed" }; + +export type DriverInstanceTerminalState = + | { readonly phase: "open" } + | { readonly phase: "selected"; readonly terminal: DriverInstanceTerminal } + | { readonly phase: "acked"; readonly terminal: DriverInstanceTerminal }; + +export type DriverInputOutcome = + | { readonly status: "resolved" } + | { readonly error: DriverTurnCancelledError; readonly status: "cancelled" } + | { readonly error: unknown; readonly status: "rejected" }; + +export type DriverInputSettlement = + | { readonly status: "resolved" } + | { readonly status: "cancelled" } + | { readonly failure: unknown; readonly status: "failed" }; + +interface ActiveRun { + readonly cancellation: AbortController; + cancellationClaim: DriverRunSnapshot["cancellation"]; + readonly revision: number; + readonly runId: RunId; + terminal: DriverRunTerminalState | null; +} + +interface ShutdownState { + cleanup: "completed" | "pending"; + failure: { + readonly error: RunError; + readonly runId: RunId | null; + } | null; +} + +export interface DriverShutdownSnapshot { + readonly cleanup: "completed" | "pending"; + readonly failure: { readonly error: RunError; readonly runId: RunId | null } | null; +} + +export class DriverTerminalStateMachine { + #activeRun: ActiveRun | null = null; + #instanceTerminal: DriverInstanceTerminalState = { phase: "open" }; + #lastOwnedRunId: RunId | null = null; + #lastRunTerminal: { readonly runId: RunId; readonly status: DriverRunTerminalStatus } | null = + null; + #revision = 0; + #shutdown: ShutdownState | null = null; + + beginRun(runId: RunId): DriverRunTicket { + if (this.#activeRun !== null) { + throw new Error(`Driver run ${this.#activeRun.runId} is already active.`); + } + if (this.#instanceTerminal.phase !== "open") { + throw new Error("Cannot begin a run after an instance terminal has been selected."); + } + + const cancellation = new AbortController(); + const revision = ++this.#revision; + this.#lastOwnedRunId = runId; + this.#lastRunTerminal = null; + this.#activeRun = { + cancellation, + cancellationClaim: null, + revision, + runId, + terminal: null, + }; + return { revision, runId, signal: cancellation.signal }; + } + + claimCancellation( + ticket: DriverRunTicket, + reason: string, + ): "already_claimed" | "claimed" | "terminal_selected" { + const active = this.#requireRun(ticket); + if (active.terminal !== null) { + return "terminal_selected"; + } + if (active.cancellationClaim !== null) { + return "already_claimed"; + } + + active.cancellationClaim = { reason }; + active.cancellation.abort(new DriverTurnCancelledError(reason)); + return "claimed"; + } + + currentRunId(): RunId | null { + return this.#activeRun?.runId ?? null; + } + + selectRunTerminal( + ticket: DriverRunTicket, + terminal: DriverRunTerminalIdentity, + ): "acked" | "cancelled" | "pending" | "selected" { + const active = this.#requireRun(ticket); + if (terminal.runId !== ticket.runId) { + throw new Error("Driver run terminal must target the active run."); + } + if (terminal.sourceEventId.length === 0) { + throw new Error("Driver run terminal source event ID must be non-empty."); + } + + const selected = active.terminal; + if (selected !== null) { + if (!isDeepStrictEqual(selected.value, terminal)) { + throw new Error("Driver run terminal conflicts with the selected terminal."); + } + return selected.phase === "acked" ? "acked" : "pending"; + } + + if (active.cancellationClaim !== null && terminal.status === "completed") { + return "cancelled"; + } + + active.terminal = { phase: "selected", value: structuredClone(terminal) }; + return "selected"; + } + + ackRunTerminal(ticket: DriverRunTicket, receipt: DriverEventReceipt): void { + const active = this.#requireRun(ticket); + const terminal = active.terminal; + if (terminal === null) { + throw new Error("Driver run terminal has not been selected."); + } + if (receipt.eventId !== terminal.value.sourceEventId) { + throw new Error("Driver run terminal receipt does not match the selected source event ID."); + } + if (terminal.phase === "acked") { + if (!isDeepStrictEqual(terminal.receipt, receipt)) { + throw new Error("Driver run terminal was acknowledged with a different receipt."); + } + return; + } + + active.terminal = { + phase: "acked", + receipt: structuredClone(receipt), + value: terminal.value, + }; + } + + abandonRunTerminal(ticket: DriverRunTicket, terminal: DriverRunTerminalIdentity): void { + const active = this.#requireRun(ticket); + if ( + active.terminal?.phase !== "selected" || + !isDeepStrictEqual(active.terminal.value, terminal) + ) { + throw new Error("Only the exact unpublished driver run terminal can be abandoned."); + } + active.terminal = null; + } + + acknowledgedRunTerminal(runId?: RunId): DriverRunTerminalStatus | null { + const active = this.#activeRun; + if (active?.terminal?.phase === "acked" && (runId === undefined || active.runId === runId)) { + return active.terminal.value.status; + } + return this.#lastRunTerminal !== null && + (runId === undefined || this.#lastRunTerminal.runId === runId) + ? this.#lastRunTerminal.status + : null; + } + + snapshotRun(runId?: RunId): DriverRunSnapshot | null { + const active = this.#activeRun; + if (active === null || (runId !== undefined && active.runId !== runId)) { + return null; + } + + return { + cancellation: active.cancellationClaim, + revision: active.revision, + runId: active.runId, + terminal: active.terminal, + }; + } + + settleInput(ticket: DriverRunTicket, outcome: DriverInputOutcome): DriverInputSettlement { + const active = this.#requireRun(ticket); + const terminal = active.terminal; + + if (outcome.status === "rejected") { + return { failure: outcome.error, status: "failed" }; + } + if (terminal === null || terminal.phase === "selected") { + return { + failure: new Error( + terminal === null + ? "Driver input settled without a run terminal." + : "Driver run terminal was selected but not acknowledged.", + ), + status: "failed", + }; + } + if (outcome.status === "cancelled") { + if (terminal.value.status === "cancelled") { + return { status: "cancelled" }; + } + return { + failure: new Error( + `Driver cancellation settled with a ${terminal.value.status} run terminal.`, + ), + status: "failed", + }; + } + + return terminal.value.status === "completed" + ? { status: "resolved" } + : { + failure: new Error( + `Driver input resolved after a ${terminal.value.status} run terminal.`, + ), + status: "failed", + }; + } + + releaseRun(ticket: DriverRunTicket, reason: "command_acked" | "driver_failing"): void { + const active = this.#requireRun(ticket); + if (reason === "command_acked" && active.terminal?.phase !== "acked") { + throw new Error("Cannot acknowledge a run without an acknowledged terminal."); + } + + this.#lastRunTerminal = + active.terminal?.phase === "acked" + ? { runId: active.runId, status: active.terminal.value.status } + : null; + this.#lastOwnedRunId = ticket.runId; + this.#activeRun = null; + } + + requestShutdown(): void { + this.#shutdown ??= { cleanup: "pending", failure: null }; + } + + recordFailure(error: RunError, runId = this.currentRunId()): void { + this.requestShutdown(); + this.#shutdown!.failure ??= { error: structuredClone(error), runId }; + } + + markCleanupCompleted(): void { + if (this.#shutdown === null) { + throw new Error("Driver shutdown has not been requested."); + } + this.#shutdown.cleanup = "completed"; + } + + shutdownSnapshot(): DriverShutdownSnapshot | null { + return this.#shutdown === null ? null : structuredClone(this.#shutdown); + } + + selectInstanceTerminal(terminal: DriverInstanceTerminal): "acked" | "pending" | "selected" { + const selected = this.#instanceTerminal; + if (selected.phase !== "open") { + if (!isDeepStrictEqual(selected.terminal, terminal)) { + throw new Error("Driver instance terminal conflicts with the selected terminal."); + } + return selected.phase === "acked" ? "acked" : "pending"; + } + + const frozen = structuredClone(terminal); + this.#instanceTerminal = { phase: "selected", terminal: frozen }; + return "selected"; + } + + ackInstanceTerminal(terminal: DriverInstanceTerminal): void { + const selected = this.#instanceTerminal; + if (selected.phase === "open") { + throw new Error("Driver instance terminal has not been selected."); + } + if (!isDeepStrictEqual(selected.terminal, terminal)) { + throw new Error("Driver instance terminal acknowledgement conflicts with its selection."); + } + this.#instanceTerminal = { phase: "acked", terminal: selected.terminal }; + } + + abandonInstanceTerminal(terminal: DriverInstanceTerminal): void { + const selected = this.#instanceTerminal; + if (selected.phase !== "selected" || !isDeepStrictEqual(selected.terminal, terminal)) { + throw new Error("Only the exact unpublished driver instance terminal can be abandoned."); + } + this.#instanceTerminal = { phase: "open" }; + } + + snapshotInstance(): DriverInstanceTerminalState { + return this.#instanceTerminal; + } + + terminalRunId(fallback: RunId | null = null): RunId | null { + const instanceTerminal = this.#instanceTerminal; + if (instanceTerminal.phase !== "open") { + return instanceTerminal.terminal.runId; + } + + return ( + this.#shutdown?.failure?.runId ?? this.#activeRun?.runId ?? this.#lastOwnedRunId ?? fallback + ); + } + + #requireRun(ticket: DriverRunTicket): ActiveRun { + const active = this.#activeRun; + if ( + active === null || + active.revision !== ticket.revision || + active.runId !== ticket.runId || + active.cancellation.signal !== ticket.signal + ) { + throw new Error("Driver run ticket is stale."); + } + return active; + } +} diff --git a/src/core/external-tool-effect-settlement.ts b/src/core/external-tool-effect-settlement.ts new file mode 100644 index 0000000..fcb9559 --- /dev/null +++ b/src/core/external-tool-effect-settlement.ts @@ -0,0 +1,75 @@ +import type { + McpExecuteCommand, + McpExternalToolEffectSettlement, + McpExternalToolExecutionResult, +} from "../runtime-command"; +import { + measureRuntimeCommandJson, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, +} from "../runtime-command"; +const OVERSIZED_MCP_OUTPUT = + "MCP tool output was omitted because its durable settlement exceeded the 1044480-byte limit."; + +type McpResultIdentity = Pick; + +function createOmittedSettlement( + identity: McpResultIdentity, +): Extract { + return { + kind: "succeeded", + result: { isError: true, outputText: OVERSIZED_MCP_OUTPUT, ...identity }, + }; +} + +/** Proves the fixed result identity is settleable before the provider may run. */ +export function requireDurableMcpResultIdentity(command: McpResultIdentity): McpResultIdentity { + const identity = { + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }; + + if ( + measureRuntimeCommandJson(createOmittedSettlement(identity)) > + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES + ) { + throw new RangeError("MCP command identity exceeds the durable settlement byte limit."); + } + + return identity; +} + +/** + * Keeps a known provider result durably settleable without retaining unbounded + * diagnostic data or presenting an omitted response as a successful tool result. + */ +export function createDurableMcpSucceededSettlement( + execution: McpExternalToolExecutionResult, + identity: McpResultIdentity, +): Extract { + const result = { + ...(execution.isError === undefined ? {} : { isError: execution.isError }), + outputText: execution.outputText, + ...identity, + }; + const settlement = { + kind: "succeeded", + ...(execution.providerReceiptJson === undefined + ? {} + : { providerReceiptJson: execution.providerReceiptJson }), + result, + } as const; + + if (measureRuntimeCommandJson(settlement) <= RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES) { + return settlement; + } + + const withoutReceipt = { kind: "succeeded", result } as const; + if ( + measureRuntimeCommandJson(withoutReceipt) <= RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES + ) { + return withoutReceipt; + } + + return createOmittedSettlement(identity); +} diff --git a/src/host-ports/index.ts b/src/host-ports/index.ts index b7374ab..b2bb0c2 100644 --- a/src/host-ports/index.ts +++ b/src/host-ports/index.ts @@ -3,44 +3,43 @@ import type { DriverExecutionInput } from "../protocol/execution"; import type { RunId } from "../protocol/id"; import type { DriverEventBatchOutput } from "../protocol/orpc"; import type { + DriverCommandUpdate, McpExecuteCommand, - McpExecuteCommandResult, McpExternalToolEffectClaim, McpExternalToolEffectExecution, + McpExternalToolEffectSettlement, + McpExternalToolEffectState, McpExternalToolExecutionResult, - RunError, RuntimeCommand, - RuntimeCommandResult, } from "../runtime-command"; +/** Maximum provider-call duration after the durable MCP claim commits. */ +export const AGENT_DRIVER_MCP_EXECUTE_TIMEOUT_MS = 60_000; + export interface AgentDriverCommandSource { nextCommand(signal: AbortSignal): Promise; } export interface AgentDriverEventSink { claimExternalToolEffect?( - input: { commandId: string }, + input: { claimToken: string; commandId: string }, signal: AbortSignal, ): Promise; - commandUpdate( - input: { - commandId: string; - error?: RunError; - result?: RuntimeCommandResult; - status: "accepted" | "cancelled" | "completed" | "failed"; - }, + commandUpdate(input: DriverCommandUpdate, signal: AbortSignal): Promise; + observeExternalToolEffect?( + input: { commandId: string }, signal: AbortSignal, - ): Promise; - completeExternalToolEffect?( + ): Promise; + settleExternalToolEffect?( input: { + claimToken: string; commandId: string; - providerReceiptJson?: string | null | undefined; - result: McpExecuteCommandResult; + effectId: string; + settlement: McpExternalToolEffectSettlement; }, signal: AbortSignal, - ): Promise; + ): Promise; currentRunId(): RunId | null; - markExternalToolEffectUnknown?(input: { commandId: string }, signal: AbortSignal): Promise; pushEvents(input: { events: DriverEventInput[]; signal?: AbortSignal; @@ -72,10 +71,15 @@ export interface DriverPermissionRequest { } export interface AgentDriverMcpExecution extends AsyncDisposable { + /** + * Runs after the durable effect claim commits. Implementations must bound + * this call independently instead of reusing the cancellable prepare signal. + */ execute(effect: McpExternalToolEffectExecution): Promise; } export interface AgentDriverMcpPort { + /** Prepares provider state without invoking the external tool. */ prepare(command: McpExecuteCommand, signal: AbortSignal): Promise; } @@ -95,11 +99,14 @@ export interface AgentDriverSkillPort { } export interface AgentDriverFilePort { - reportChanged(input: { - change: "delete" | "upsert"; - path: string; - reason: string; - }): Promise; + reportChanged( + input: { + change: "delete" | "upsert"; + path: string; + reason: string; + }, + signal: AbortSignal, + ): Promise; } export interface AgentDriverHostPorts { diff --git a/src/index.ts b/src/index.ts index 1fc087c..212e484 100644 --- a/src/index.ts +++ b/src/index.ts @@ -140,10 +140,15 @@ export type { MessageId, RunId, } from "./protocol/id"; -export { parseRuntimeCommand } from "./runtime-command"; +export { + createMcpUnknownEffectRunError, + createMcpUnsettledEffectRunError, + parseRuntimeCommand, +} from "./runtime-command"; export type { DriverCapability, DriverCapabilityId, + DriverCommandUpdate, InputStartCommand, InputStartCommandResult, McpExecuteCommand, diff --git a/src/infrastructure/runtime/driver-instance-socket.ts b/src/infrastructure/runtime/driver-instance-socket.ts index e5b4ba6..a287d4c 100644 --- a/src/infrastructure/runtime/driver-instance-socket.ts +++ b/src/infrastructure/runtime/driver-instance-socket.ts @@ -1,9 +1,21 @@ -import { isDeepStrictEqual } from "node:util"; - import { createORPCClient } from "@orpc/client"; import { RPCLink } from "@orpc/client/websocket"; -import { assertDriverEventReceiptPrefix } from "../../core/driver-runtime-io"; +import { + assertDriverEventReceiptPrefix, + assertIsolatedRunTerminalBatch, +} from "../../core/driver-runtime-io"; +import type { DriverRuntimeIo } from "../../core/driver-runtime-io"; +import type { DriverRunTerminalBarrier } from "../../core/driver-runtime-io"; +import { + DriverTerminalStateMachine, + type DriverInputOutcome, + type DriverInputSettlement, + type DriverInstanceTerminal, + type DriverRunSnapshot, + type DriverRunTerminalIdentity, + type DriverRunTicket, +} from "../../core/driver-terminal-state"; import type { DriverBootPayload } from "../../protocol/boot"; import type { DriverEventEnvelope, DriverEventInput } from "../../protocol/events"; import type { RunId } from "../../protocol/id"; @@ -12,6 +24,7 @@ import type { DriverFailureInput, DriverEventBatchOutput, DriverExternalToolEffectClaimOutput, + DriverExternalToolEffectState, DriverHeartbeatInput, DriverHeartbeatOutput, DriverHelloInput, @@ -22,12 +35,12 @@ import type { } from "../../protocol/orpc"; import type { DriverRuntimeClient } from "../../protocol/orpc"; import type { - McpExecuteCommandResult, + DriverCommandUpdate, McpExternalToolEffectClaim, - RunError, + McpExternalToolEffectState, RuntimeCommand, - RuntimeCommandResult, } from "../../runtime-command"; +import { normalizeDurableRunError } from "../../runtime-command"; import { raceWithAbort } from "../../utils/async"; import { dialDriverControlSocket } from "./driver-control-dial"; import type { DriverWireSocket } from "./driver-control-dial"; @@ -39,31 +52,6 @@ interface DriverInstanceSocketHandlers { onClose: (code: number, reason: string) => void; } -type RunTerminalDelivery = - | { - delivered: boolean; - readonly runGeneration: number; - status: "completed"; - task?: Promise; - } - | { - delivered: boolean; - error: DriverFailureInput["error"]; - readonly runGeneration: number; - status: "failed"; - task?: Promise; - }; - -type RunEventTerminalStatus = "cancelled" | "completed" | "failed"; - -interface RunEventTerminalSelection { - delivered: boolean; - readonly event: Pick; - readonly runId: RunId; - readonly status: RunEventTerminalStatus; - task?: Promise; -} - interface PreparedEventPush { readonly client: DriverRuntimeClient; readonly delivery: DriverEventEnvelope["event"]["delivery"] | undefined; @@ -71,11 +59,17 @@ interface PreparedEventPush { readonly generation: number; readonly hasRunScopedEvent: boolean; readonly maxBatchSize: number; - readonly runGeneration: number; - readonly selection: RunEventTerminalSelection | null; + readonly runTicket: DriverRunTicket | null; + readonly terminal: DriverRunTerminalIdentity | null; + readonly terminalSelection: "acked" | "pending" | "selected" | null; readonly signal: AbortSignal | undefined; } +interface RunEventTerminalTask { + readonly task: Promise; + readonly ticket: DriverRunTicket; +} + const DRIVER_RPC_TIMEOUT_MS = 10_000; const MAX_WEBSOCKET_CLOSE_REASON_BYTES = 123; @@ -96,16 +90,17 @@ function toWebSocketCloseReason(reason: string): string { } export class DriverInstanceSocket { - #activeRunId: RunId | null = null; + #activeRunTicket: DriverRunTicket | null = null; #client: DriverRuntimeClient | null = null; #connectionGeneration = 0; #connectAbortController: AbortController | null = null; #deliveryTail: Promise = Promise.resolve(); #eventBatchMaxSize: number | null = null; #rpcAbortController = new AbortController(); - #runGeneration = 0; - #runEventTerminal: RunEventTerminalSelection | null = null; - #runTerminal: RunTerminalDelivery | null = null; + #instanceTerminalTask: Promise | null = null; + #runEventTerminalTask: RunEventTerminalTask | null = null; + #runTerminalBarrier: DriverRunTerminalBarrier | null = null; + readonly #terminalState = new DriverTerminalStateMachine(); private readonly handlers: DriverInstanceSocketHandlers; private readonly payload: DriverBootPayload; #socket: DriverWireSocket | null = null; @@ -180,57 +175,49 @@ export class DriverInstanceSocket { } } - beginRun(runId: RunId): void { - if (this.#activeRunId !== null) { - throw new Error("Cannot begin a run while another run is active."); - } - if (this.#runTerminal !== null) { - throw new Error("Cannot begin a run after a control terminal has been selected."); - } + beginRun(runId: RunId): DriverRunTicket { + const ticket = this.#terminalState.beginRun(runId); + this.#activeRunTicket = ticket; + return ticket; + } - this.#runGeneration += 1; - this.#activeRunId = runId; - this.#runEventTerminal = null; + claimRunCancellation( + ticket: DriverRunTicket, + reason: string, + ): "already_claimed" | "claimed" | "terminal_selected" { + return this.#terminalState.claimCancellation(ticket, reason); } - endRun(runId: RunId): void { - if (this.#activeRunId === runId) { - this.#runGeneration += 1; - this.#activeRunId = null; + releaseRun(ticket: DriverRunTicket, reason: "command_acked" | "driver_failing"): void { + this.#terminalState.releaseRun(ticket, reason); + if (this.#activeRunTicket === ticket) { + this.#activeRunTicket = null; + this.#runEventTerminalTask = null; } } currentRunId(): RunId | null { - return this.#activeRunId; + return this.#terminalState.currentRunId(); } - runEventTerminal(runId: RunId): "cancelled" | "completed" | "failed" | null { - const terminal = this.#runEventTerminal; - return terminal?.runId === runId && terminal.delivered ? terminal.status : null; + runSnapshot(runId?: RunId): DriverRunSnapshot | null { + return this.#terminalState.snapshotRun(runId); } - selectedRunEventTerminal(runId: RunId): "cancelled" | "completed" | "failed" | null { - const terminal = this.#runEventTerminal; - return terminal?.runId === runId ? terminal.status : null; + settleRunInput(ticket: DriverRunTicket, outcome: DriverInputOutcome): DriverInputSettlement { + return this.#terminalState.settleInput(ticket, outcome); } - async commandUpdate( - input: { - commandId: string; - error?: RunError; - result?: RuntimeCommandResult; - status: "accepted" | "cancelled" | "completed" | "delivered" | "expired" | "failed"; - }, - signal: AbortSignal, - ): Promise { + async commandUpdate(input: DriverCommandUpdate, signal: AbortSignal): Promise { + const update = + input.status === "failed" + ? { ...input, error: normalizeDurableRunError(input.error) } + : input; driverRuntimeRpcSchemas.driver.commandUpdate.output.parse( await this.#requireClient().driver.commandUpdate( { - commandId: input.commandId, + ...update, driverInstanceId: this.payload.driverInstanceId, - ...(input.error === undefined ? {} : { error: input.error }), - status: input.status, - ...(input.result === undefined ? {} : { result: input.result }), }, this.#rpcOptions(signal), ), @@ -238,13 +225,14 @@ export class DriverInstanceSocket { } async claimExternalToolEffect( - input: { commandId: string }, + input: { claimToken: string; commandId: string }, signal: AbortSignal, ): Promise { const result: DriverExternalToolEffectClaimOutput = driverRuntimeRpcSchemas.driver.claimExternalToolEffect.output.parse( await this.#requireClient().driver.claimExternalToolEffect( { + claimToken: input.claimToken, commandId: input.commandId, driverInstanceId: this.payload.driverInstanceId, }, @@ -255,27 +243,43 @@ export class DriverInstanceSocket { return result; } - async completeExternalToolEffect( - input: { - commandId: string; - providerReceiptJson?: string | null | undefined; - result: McpExecuteCommandResult; - }, + async observeExternalToolEffect( + input: Parameters[0], signal: AbortSignal, - ): Promise { - driverRuntimeRpcSchemas.driver.completeExternalToolEffect.output.parse( - await this.#requireClient().driver.completeExternalToolEffect( - { - commandId: input.commandId, - driverInstanceId: this.payload.driverInstanceId, - ...(input.providerReceiptJson === undefined - ? {} - : { providerReceiptJson: input.providerReceiptJson }), - result: input.result, - }, - this.#effectSettlementRpcOptions(signal), - ), - ); + ): Promise { + const result: DriverExternalToolEffectState = + driverRuntimeRpcSchemas.driver.observeExternalToolEffect.output.parse( + await this.#requireClient().driver.observeExternalToolEffect( + { + commandId: input.commandId, + driverInstanceId: this.payload.driverInstanceId, + }, + this.#rpcOptions(signal), + ), + ); + + return result; + } + + async settleExternalToolEffect( + input: Parameters[0], + signal: AbortSignal, + ): Promise { + const result: DriverExternalToolEffectState = + driverRuntimeRpcSchemas.driver.settleExternalToolEffect.output.parse( + await this.#requireClient().driver.settleExternalToolEffect( + { + claimToken: input.claimToken, + commandId: input.commandId, + driverInstanceId: this.payload.driverInstanceId, + effectId: input.effectId, + settlement: input.settlement, + }, + this.#effectSettlementRpcOptions(signal), + ), + ); + + return result; } completeRun(signal?: AbortSignal): Promise { @@ -332,31 +336,83 @@ export class DriverInstanceSocket { events: DriverEventInput[]; signal?: AbortSignal; }): Promise { - const prepared = this.#prepareEventPush(input); - const task = this.#enqueueDelivery(() => this.#deliverEventPush(prepared), input.signal); - const selection = prepared.selection; + input.signal?.throwIfAborted(); + const ownedInput = { ...input, events: structuredClone(input.events) }; + assertIsolatedRunTerminalBatch(ownedInput.events); + const barrier = this.#runTerminalBarrier; + if (barrier !== null) { + const pending = barrier(ownedInput.events); + if (pending !== undefined) { + await pending; + } + } + const prepared = this.#prepareEventPush(ownedInput); + const { runTicket, terminal, terminalSelection } = prepared; + + if (terminalSelection === "acked") { + const snapshot = this.#terminalState.snapshotRun(terminal!.runId); + const receipt = snapshot?.terminal?.phase === "acked" ? snapshot.terminal.receipt : null; + if (receipt === null) { + throw new Error("Driver run terminal acknowledgement is unavailable."); + } + return { accepted: [receipt] }; + } + + if (terminalSelection === "pending" && this.#runEventTerminalTask !== null) { + if (this.#runEventTerminalTask.ticket !== runTicket) { + throw new Error("Driver active run changed during terminal delivery."); + } + return { accepted: [await this.#runEventTerminalTask.task] }; + } - if (selection !== null) { - selection.task = task; + const task = this.#enqueueDelivery(() => this.#deliverEventPush(prepared), input.signal); + const terminalTask = + terminal === null || runTicket === null + ? null + : { + task: task.then((result) => { + if (result.accepted.length !== prepared.events.length) { + throw new Error("Driver run terminal batch was not fully acknowledged."); + } + const receipt = result.accepted.at(-1); + if (receipt === undefined) { + throw new Error("Driver run terminal receipt is missing."); + } + this.#terminalState.ackRunTerminal(runTicket, receipt); + return receipt; + }), + ticket: runTicket, + }; + + if (terminalTask !== null) { + this.#runEventTerminalTask = terminalTask; + void terminalTask.task.catch(() => {}); } try { const result = await task; - if ( - selection !== null && - result.accepted.length === prepared.events.length && - this.#runEventTerminal === selection - ) { - selection.delivered = true; - } + await terminalTask?.task; return result; } finally { - if (selection?.task === task) { - delete selection.task; + if (this.#runEventTerminalTask === terminalTask) { + this.#runEventTerminalTask = null; } } } + registerRunTerminalBarrier(barrier: DriverRunTerminalBarrier): () => void { + if (this.#runTerminalBarrier !== null) { + throw new Error("Driver run terminal barrier is already registered."); + } + + this.#runTerminalBarrier = barrier; + return () => { + if (this.#runTerminalBarrier === barrier) { + this.#runTerminalBarrier = null; + } + }; + } + #prepareEventPush(input: { events: DriverEventInput[]; signal?: AbortSignal; @@ -368,16 +424,24 @@ export class DriverInstanceSocket { throw new Error("Driver hello must complete before events are pushed."); } - const activeRunId = this.#activeRunId; - const events = structuredClone(input.events).flatMap((event) => - toDriverEventEnvelopes(this.payload, event, activeRunId), - ); + const runTicket = this.#activeRunTicket; + const activeRunId = runTicket?.runId ?? null; + const selectedTerminal = + runTicket === null ? null : this.#terminalState.snapshotRun(runTicket.runId)?.terminal?.value; + const events = structuredClone(input.events) + .map((event) => + selectedTerminal !== null && + selectedTerminal !== undefined && + event.sourceEventId === undefined && + (event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed") + ? { ...event, sourceEventId: selectedTerminal.sourceEventId } + : event, + ) + .flatMap((event) => toDriverEventEnvelopes(this.payload, event, activeRunId)); const delivery = events[0]?.event.delivery; - let terminal: { - event: Pick; - runId: RunId; - status: RunEventTerminalStatus; - } | null = null; + let terminal: DriverRunTerminalIdentity | null = null; let terminalIndex = -1; let hasRunScopedEvent = false; @@ -413,8 +477,13 @@ export class DriverInstanceSocket { } terminal = { - event: { kind: event.kind, payload: structuredClone(event.payload) }, + event: { + kind: event.kind, + payload: structuredClone(event.payload), + sourceEventId: event.sourceEventId ?? event.id, + }, runId: event.runId, + sourceEventId: event.sourceEventId ?? event.id, status, }; terminalIndex = index; @@ -427,31 +496,21 @@ export class DriverInstanceSocket { throw new Error("Driver run terminal must use lossless delivery."); } - let selection: RunEventTerminalSelection | null = null; - if (hasRunScopedEvent && this.#runEventTerminal !== null) { - const selected = this.#runEventTerminal; - const retryingSelectedEvent = - events.length === 1 && - terminal !== null && - selected !== null && - !selected.delivered && - selected.task === undefined && - selected.runId === terminal.runId && - selected.status === terminal.status && - isDeepStrictEqual(selected.event, terminal.event); - - if (!retryingSelectedEvent) { - throw new Error("Driver event cannot target a terminated run."); + let terminalSelection: PreparedEventPush["terminalSelection"] = null; + if (terminal !== null) { + if (runTicket === null) { + throw new Error("Driver run terminal must target the active run."); } - selection = selected; - } else if (terminal !== null) { - selection = { - delivered: false, - event: terminal.event, - runId: terminal.runId, - status: terminal.status, - }; - this.#runEventTerminal = selection; + const selection = this.#terminalState.selectRunTerminal(runTicket, terminal); + if (selection === "cancelled") { + throw new Error("Driver completed terminal lost the cancellation race."); + } + terminalSelection = selection; + if (terminalSelection !== "selected" && events.length !== 1) { + throw new Error("A selected driver run terminal can only be retried by itself."); + } + } else if (hasRunScopedEvent && this.#terminalState.snapshotRun()?.terminal !== null) { + throw new Error("Driver event cannot target a terminated run."); } return { @@ -461,9 +520,10 @@ export class DriverInstanceSocket { generation: this.#connectionGeneration, hasRunScopedEvent, maxBatchSize, - runGeneration: this.#runGeneration, - selection, + runTicket, signal: input.signal, + terminal, + terminalSelection, }; } @@ -471,7 +531,7 @@ export class DriverInstanceSocket { const { client, delivery, events, generation, maxBatchSize, signal } = prepared; const accepted: DriverEventBatchOutput["accepted"][number][] = []; this.#assertConnection(client, generation, "event delivery"); - this.#assertRunGeneration(prepared); + this.#assertRunTicket(prepared); const rpcOptions = this.#rpcOptions(signal); for (let index = 0; index < events.length; index += maxBatchSize) { @@ -488,7 +548,7 @@ export class DriverInstanceSocket { ), ); this.#assertConnection(client, generation, "event delivery"); - this.#assertRunGeneration(prepared); + this.#assertRunTicket(prepared); assertDriverEventReceiptPrefix( remaining.map((envelope) => envelope.event), @@ -571,21 +631,6 @@ export class DriverInstanceSocket { return result.command; } - async markExternalToolEffectUnknown( - input: { commandId: string }, - signal: AbortSignal, - ): Promise { - driverRuntimeRpcSchemas.driver.markExternalToolEffectUnknown.output.parse( - await this.#requireClient().driver.markExternalToolEffectUnknown( - { - commandId: input.commandId, - driverInstanceId: this.payload.driverInstanceId, - }, - this.#effectSettlementRpcOptions(signal), - ), - ); - } - #deliverRunTerminal( status: "completed" | "failed", error?: DriverFailureInput["error"], @@ -595,32 +640,27 @@ export class DriverInstanceSocket { return Promise.reject(new Error("Failed run terminal requires an error.")); } - let terminal = this.#runTerminal; - - if (terminal === null) { - terminal = - status === "completed" - ? { delivered: false, runGeneration: this.#runGeneration, status } - : { - delivered: false, - error: structuredClone(error!), - runGeneration: this.#runGeneration, - status, - }; - this.#runTerminal = terminal; + const runId = this.#terminalState.terminalRunId(this.payload.execution.configRevision.runId); + if (runId === null) { + return Promise.reject(new Error("Driver run terminal requires an exact run ID.")); } - if (terminal.status !== status) { - return Promise.resolve(); - } - if (terminal.status === "failed" && !isDeepStrictEqual(terminal.error, error)) { - return Promise.reject(new Error("Failed run terminal was retried with a different error.")); + const terminal: DriverInstanceTerminal = + status === "completed" + ? { runId, status } + : { error: normalizeDurableRunError(structuredClone(error!)), runId, status }; + let selection: "acked" | "pending" | "selected"; + try { + selection = this.#terminalState.selectInstanceTerminal(terminal); + } catch (selectionError) { + return Promise.reject(selectionError); } - if (terminal.delivered) { + + if (selection === "acked") { return Promise.resolve(); } - if (terminal.task !== undefined) { - return terminal.task; + if (selection === "pending" && this.#instanceTerminalTask !== null) { + return this.#instanceTerminalTask; } const client = this.#client; @@ -630,13 +670,13 @@ export class DriverInstanceSocket { throw new Error("Driver instance socket is not connected."); } this.#assertConnection(client, generation, "run terminal delivery"); - this.#assertRunTerminal(terminal); + this.#assertInstanceTerminal(terminal); const options = this.#rpcOptions(signal); if (terminal.status === "completed") { driverRuntimeRpcSchemas.driver.completeRun.output.parse( await client.driver.completeRun( - { driverInstanceId: this.payload.driverInstanceId }, + { driverInstanceId: this.payload.driverInstanceId, runId: terminal.runId }, options, ), ); @@ -646,6 +686,7 @@ export class DriverInstanceSocket { { driverInstanceId: this.payload.driverInstanceId, error: structuredClone(terminal.error), + runId: terminal.runId, }, options, ), @@ -653,19 +694,19 @@ export class DriverInstanceSocket { } this.#assertConnection(client, generation, "run terminal delivery"); - this.#assertRunTerminal(terminal); + this.#assertInstanceTerminal(terminal); + this.#terminalState.ackInstanceTerminal(terminal); }, signal); - terminal.task = task; + this.#instanceTerminalTask = task; void task.then( () => { - if (this.#runTerminal === terminal && terminal.task === task) { - terminal.delivered = true; - delete terminal.task; + if (this.#instanceTerminalTask === task) { + this.#instanceTerminalTask = null; } }, () => { - if (this.#runTerminal === terminal && terminal.task === task) { - delete terminal.task; + if (this.#instanceTerminalTask === task) { + this.#instanceTerminalTask = null; } }, ); @@ -678,16 +719,18 @@ export class DriverInstanceSocket { } } - #assertRunGeneration(prepared: PreparedEventPush): void { - if (prepared.hasRunScopedEvent && prepared.runGeneration !== this.#runGeneration) { + #assertRunTicket(prepared: PreparedEventPush): void { + if ( + prepared.hasRunScopedEvent && + (prepared.runTicket === null || + this.#terminalState.snapshotRun()?.revision !== prepared.runTicket.revision) + ) { throw new Error("Driver active run changed during event delivery."); } } - #assertRunTerminal(terminal: RunTerminalDelivery): void { - if (terminal.runGeneration !== this.#runGeneration || this.#runTerminal !== terminal) { - throw new Error("Driver active run changed during run terminal delivery."); - } + #assertInstanceTerminal(terminal: DriverInstanceTerminal): void { + this.#terminalState.selectInstanceTerminal(terminal); } #enqueueDelivery(operation: () => Promise, signal?: AbortSignal): Promise { diff --git a/src/observability/driver-debug-events.ts b/src/observability/driver-debug-events.ts index e01c4d6..71ad4fe 100644 --- a/src/observability/driver-debug-events.ts +++ b/src/observability/driver-debug-events.ts @@ -71,10 +71,14 @@ function addDriverEventToSummary(summary: DriverEventBatchSummary, event: Driver if (event.kind === "tool.call.updated") { const payload = isUnknownRecord(event.payload) ? event.payload : {}; - if (typeof payload["rawInput"] === "string") { + if (typeof payload["rawInput"] === "string" || typeof payload["rawInputDelta"] === "string") { summary.toolCallArgsCount += 1; } - if (typeof payload["rawOutput"] === "string" || typeof payload["content"] === "string") { + if ( + typeof payload["rawOutput"] === "string" || + typeof payload["rawOutputDelta"] === "string" || + typeof payload["content"] === "string" + ) { summary.toolCallResultCount += 1; } } diff --git a/src/observability/driver-debug-runtime.ts b/src/observability/driver-debug-runtime.ts index 5194b7f..38853be 100644 --- a/src/observability/driver-debug-runtime.ts +++ b/src/observability/driver-debug-runtime.ts @@ -28,6 +28,7 @@ export function summarizeRuntimeCommand(command: RuntimeCommand): Record + | Omit, "runId"> + | Omit, "runId"> + | Omit, "runId">; + type CmaInboundType = | "user.custom_tool_result" | "user.interrupt" @@ -184,7 +190,7 @@ export function parseCmaInboundEvent(input: unknown): CmaInboundEvent { } } -export function projectCmaInboundToDriverCommand(input: unknown): RuntimeCommand { +export function projectCmaInboundToDriverCommand(input: unknown): CmaProjectedDriverCommand { const event = parseCmaInboundEvent(input); switch (event.type) { diff --git a/src/projections/cma/index.ts b/src/projections/cma/index.ts index 47165bb..736a7b2 100644 --- a/src/projections/cma/index.ts +++ b/src/projections/cma/index.ts @@ -4,6 +4,7 @@ export { parseCmaInboundEvent, projectCmaInboundToDriverCommand, type CmaInboundEvent, + type CmaProjectedDriverCommand, type CmaUserCustomToolResultEvent, type CmaUserInterruptEvent, type CmaUserMessageEvent, diff --git a/src/protocol/boot/index.ts b/src/protocol/boot/index.ts index cda3246..fbc1c6e 100644 --- a/src/protocol/boot/index.ts +++ b/src/protocol/boot/index.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { parseTraceparent } from "vestig"; import type { DriverInstanceId } from "../id"; import type { JsonObject } from "../json"; @@ -41,11 +42,11 @@ export type { } from "./host-snapshot"; /** - * Version 2 requires the durable external-tool-effect RPCs. Refusing an older + * Version 3 requires the durable external-tool-effect RPCs. Refusing an older * Driver is safer than letting it invoke an MCP tool without the persistence * fence during a rolling deployment. */ -export const DRIVER_PROTOCOL_VERSION = 2; +export const DRIVER_PROTOCOL_VERSION = 3; export const DRIVER_CONTROL_PORT_MIN = 20_000; export const DRIVER_CONTROL_PORT_MAX = 59_999; export const DRIVER_BOOT_PAYLOAD_ENV_NAME = "MOSOO_DRIVER_BOOT_PAYLOAD"; @@ -76,6 +77,31 @@ const runtimeTransportByRuntime = { const controlUrlSchema = z .url() .refine((value) => /^(?:https?|wss?):/iu.test(value), "must use http, https, ws, or wss"); +const traceparentPattern = /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/u; + +export function parseDriverTraceparent(value: string): { traceId: string; spanId: string } { + const parsed = parseTraceparent(value); + + if ( + parsed === null || + !traceparentPattern.test(value) || + parsed.traceId === "0".repeat(32) || + parsed.spanId === "0".repeat(16) + ) { + throw new TypeError("traceparent must be a valid W3C traceparent header"); + } + + return parsed; +} + +const traceparentSchema = nonEmptyStringSchema.refine((value) => { + try { + parseDriverTraceparent(value); + return true; + } catch { + return false; + } +}, "must be a valid W3C traceparent header"); function omitUndefinedProperties>(value: Value): Value { return Object.fromEntries( @@ -300,7 +326,7 @@ const driverBootPayloadSchema = ownObjectSchema({ runtime: z.enum(SUPPORTED_DRIVER_RUNTIMES), runtimeTransport: z.enum(SUPPORTED_DRIVER_RUNTIME_TRANSPORTS), sandboxId: createDriverIdSchema(), - traceparent: nonEmptyStringSchema, + traceparent: traceparentSchema, }).superRefine((payload, context) => { if (payload.sandboxId !== payload.execution.session.context.sandboxId) { context.addIssue({ diff --git a/src/protocol/events/index.ts b/src/protocol/events/index.ts index 3c1ee95..ef5187f 100644 --- a/src/protocol/events/index.ts +++ b/src/protocol/events/index.ts @@ -1,8 +1,15 @@ import { timestampSchema } from "../../contract/common"; import { parseRuntimeEventEnvelope } from "./runtime-events"; import type { RuntimeEventEnvelope, RuntimeEventInputDraft } from "./runtime-events"; +import { requireExactKeys } from "./runtime-event-validation"; import type { RunId } from "../id"; +export { + RUNTIME_EVENT_KINDS, + RUNTIME_EVENT_SCHEMA_VERSION, + toRuntimeEventInput, +} from "./runtime-events"; + export type DriverEvent = RuntimeEventEnvelope; type DriverEventInputDraft = Omit & { readonly runId?: RunId | null | undefined; @@ -23,6 +30,7 @@ export function parseDriverEventEnvelope(input: unknown): DriverEventEnvelope { if (!isRecord(input)) { throw new TypeError("Driver event envelope must be an object."); } + requireExactKeys(input, new Set(["event", "eventId", "occurredAt"]), "Driver event envelope"); const eventId = input["eventId"]; const occurredAt = input["occurredAt"]; diff --git a/src/protocol/events/runtime-event-types.ts b/src/protocol/events/runtime-event-types.ts index c819e38..3a0fb85 100644 --- a/src/protocol/events/runtime-event-types.ts +++ b/src/protocol/events/runtime-event-types.ts @@ -1,6 +1,6 @@ import type { DriverInstanceId, EventId, RunId, SessionId } from "../id"; -export const RUNTIME_EVENT_SCHEMA_VERSION = "2026-05-26"; +export const RUNTIME_EVENT_SCHEMA_VERSION = "2026-08-29"; export const RUNTIME_EVENT_KINDS = [ "account.limits.updated", @@ -124,6 +124,23 @@ export type RuntimeTimingStage = export type RuntimeEventRecord = Record; +type RuntimeToolCallInput = + | { readonly rawInput?: undefined; readonly rawInputDelta?: undefined } + | { readonly rawInput: string; readonly rawInputDelta?: undefined } + | { readonly rawInput?: undefined; readonly rawInputDelta: string }; + +type RuntimeToolCallOutput = + | { readonly rawOutput?: undefined; readonly rawOutputDelta?: undefined } + | { readonly rawOutput: string; readonly rawOutputDelta?: undefined } + | { readonly rawOutput?: undefined; readonly rawOutputDelta: string }; + +export type RuntimeToolCallUpdatedPayload = RuntimeEventRecord & + RuntimeToolCallInput & + RuntimeToolCallOutput & { + readonly status: "cancelled" | "completed" | "failed" | "running"; + readonly toolCallId: string; + }; + export interface RuntimeEventNativeRef { readonly eventName?: string | undefined; readonly itemId?: string | undefined; diff --git a/src/protocol/events/runtime-event-validation.ts b/src/protocol/events/runtime-event-validation.ts index 89d62b0..c2bb3ba 100644 --- a/src/protocol/events/runtime-event-validation.ts +++ b/src/protocol/events/runtime-event-validation.ts @@ -16,11 +16,38 @@ const payloadIdentityFields = new Set([ "traceId", ]); +export function requireExactKeys( + value: RuntimeEventRecord, + allowedKeys: ReadonlySet, + label: string, +): void { + const unexpected = Object.keys(value).find((key) => !allowedKeys.has(key)); + + if (unexpected !== undefined) { + throw new Error(`${label} ${unexpected} is not allowed.`); + } +} + export function isRuntimeEventRecord(value: unknown): value is RuntimeEventRecord { return typeof value === "object" && value !== null && !Array.isArray(value); } export function parseNativeRef(value: RuntimeEventRecord): RuntimeEventNativeRef { + requireExactKeys( + value, + new Set([ + "eventName", + "itemId", + "protocolVersion", + "provider", + "requestId", + "sequence", + "threadId", + "turnId", + ]), + "Runtime event native reference", + ); + return { ...(readOptionalString(value, "eventName", "Runtime event native reference") === undefined ? {} @@ -320,18 +347,26 @@ export function assertTimestamp(value: string, label: string): void { export function readPrimitiveRecord( value: unknown, + label: string, ): Record { - if (!isRuntimeEventRecord(value)) { + if (value === undefined) { return {}; } - return Object.fromEntries( - Object.entries(value).filter( - ([, entry]) => - entry === null || - typeof entry === "string" || - typeof entry === "number" || - typeof entry === "boolean", - ), - ) as Record; + if (!isRuntimeEventRecord(value)) { + throw new Error(`${label} must be an object.`); + } + + for (const [field, entry] of Object.entries(value)) { + if ( + entry !== null && + typeof entry !== "string" && + typeof entry !== "number" && + typeof entry !== "boolean" + ) { + throw new Error(`${label}.${field} must be a primitive JSON value.`); + } + } + + return value as Record; } diff --git a/src/protocol/events/runtime-events.ts b/src/protocol/events/runtime-events.ts index 26fa4d3..db18cfd 100644 --- a/src/protocol/events/runtime-events.ts +++ b/src/protocol/events/runtime-events.ts @@ -21,6 +21,7 @@ import { type RuntimeTimingPhase, type RuntimeTimingSource, type RuntimeTimingStage, + type RuntimeToolCallUpdatedPayload, } from "./runtime-event-types"; import { assertTimestamp, @@ -33,6 +34,7 @@ import { readOptionalDriverId, readOptionalString, readPrimitiveRecord, + requireExactKeys, requireEnumValue, requireNonNegativeInt, requireNullableTimestamp, @@ -47,7 +49,6 @@ import { requireString, requireTimestamp, } from "./runtime-event-validation"; - export * from "./runtime-event-types"; const runtimeEventKindSet = new Set(RUNTIME_EVENT_KINDS); @@ -152,6 +153,26 @@ const runStatuses = new Set([ ]); const toolStatuses = new Set(["cancelled", "completed", "failed", "running"]); const fileChangeKinds = new Set(["delete", "upsert"]); +const runtimeEventEnvelopeKeys = new Set([ + "actor", + "correlationId", + "delivery", + "driverInstanceId", + "id", + "kind", + "native", + "occurredAt", + "origin", + "payload", + "receivedAt", + "runId", + "runtimeId", + "schemaVersion", + "sessionId", + "sourceEventId", + "traceId", + "visibility", +]); function createRuntimeEvent( draft: RuntimeEventDraft, ): RuntimeEventEnvelope { @@ -181,6 +202,7 @@ export function parseRuntimeEventEnvelope(value: unknown): RuntimeEventEnvelope if (!isRuntimeEventRecord(value)) { throw new Error("Runtime event must be an object."); } + requireExactKeys(value, runtimeEventEnvelopeKeys, "Runtime event"); if (value["schemaVersion"] !== RUNTIME_EVENT_SCHEMA_VERSION) { throw new Error("Runtime event schema version is unsupported."); @@ -253,6 +275,15 @@ export function parseRuntimeEventEnvelope(value: unknown): RuntimeEventEnvelope value["payload"], ); + let native; + + if ("native" in value && value["native"] !== undefined) { + if (!isRuntimeEventRecord(value["native"])) { + throw new Error("Runtime event native reference must be an object."); + } + native = parseNativeRef(value["native"]); + } + return { actor, ...(correlationId === undefined ? {} : { correlationId }), @@ -260,7 +291,7 @@ export function parseRuntimeEventEnvelope(value: unknown): RuntimeEventEnvelope ...(driverInstanceId === undefined ? {} : { driverInstanceId }), id, kind, - ...(isRuntimeEventRecord(value["native"]) ? { native: parseNativeRef(value["native"]) } : {}), + ...(native === undefined ? {} : { native }), occurredAt, origin, payload, @@ -454,7 +485,7 @@ function admitRuntimeEventPayload( throw new Error(`Runtime event ${context.kind} payload must include text content.`); } - requireOptionalString(record, "messageId", context.kind); + requireString(record, "messageId", context.kind); requireOptionalEnumValue( record, "level", @@ -471,13 +502,17 @@ function admitRuntimeEventPayload( } case "message.cancelled": case "message.completed": - case "message.started": + case "message.started": { + const record = requirePayloadRecord(context.kind, canonicalPayload); + requireString(record, "messageId", context.kind); + requireOptionalEnumValue(record, "role", new Set(["agent", "user"]), context.kind); + return omitPayloadIdentity(record); + } case "thought.cancelled": case "thought.completed": case "thought.started": { const record = requirePayloadRecord(context.kind, canonicalPayload); - requireOptionalString(record, "messageId", context.kind); - requireOptionalString(record, "thoughtId", context.kind); + requireString(record, "thoughtId", context.kind); requireOptionalEnumValue(record, "role", new Set(["agent", "user"]), context.kind); return omitPayloadIdentity(record); } @@ -496,7 +531,7 @@ function admitRuntimeEventPayload( throw new Error("Runtime event thought.delta payload must include text content."); } - requireOptionalString(record, "thoughtId", context.kind); + requireString(record, "thoughtId", context.kind); return omitPayloadIdentity(record); } case "permission.requested": { @@ -605,10 +640,22 @@ function admitRuntimeEventPayload( requireOptionalString(record, "nonExecutionKind", context.kind); requireOptionalString(record, "parentMessageId", context.kind); requireOptionalString(record, "rawInput", context.kind); + requireOptionalString(record, "rawInputDelta", context.kind); requireOptionalString(record, "rawOutput", context.kind); + requireOptionalString(record, "rawOutputDelta", context.kind); requireOptionalNullableString(record, "title", context.kind); requireOptionalString(record, "userFeedback", context.kind); - return omitPayloadIdentity(record); + if (record["rawInput"] !== undefined && record["rawInputDelta"] !== undefined) { + throw new Error( + "Runtime event tool.call.updated payload cannot contain both rawInput and rawInputDelta.", + ); + } + if (record["rawOutput"] !== undefined && record["rawOutputDelta"] !== undefined) { + throw new Error( + "Runtime event tool.call.updated payload cannot contain both rawOutput and rawOutputDelta.", + ); + } + return omitPayloadIdentity(record) as RuntimeToolCallUpdatedPayload; } default: { return isRuntimeEventRecord(canonicalPayload) @@ -633,7 +680,13 @@ function readRunPayload( } requireOptionalEnumValue(record, "lifecycle", runLifecycleStatuses, context.kind); - requireOptionalEnumValue(record, "status", runStatuses, context.kind); + if ("status" in record && record["status"] !== undefined) { + const status = requireEnumValue(record, "status", runStatuses, context.kind); + + if (!isRunStatusAllowedForKind(context.kind, status)) { + throw new Error(`Runtime event ${context.kind} payload status is inconsistent.`); + } + } requireOptionalString(record, "inputSummary", context.kind); requireOptionalString(record, "reason", context.kind); requireOptionalString(record, "requestedBy", context.kind); @@ -644,6 +697,16 @@ function readRunPayload( requireOptionalTimestamp(record, "completedAt", context.kind); requireOptionalTimestamp(record, "startedAt", context.kind); + if (context.kind === "run.completed") { + if ("finalMessageId" in record) { + requireString(record, "finalMessageId", context.kind); + } + + if ("finalMessageText" in record) { + throw new Error("Runtime event run.completed payload finalMessageText is unsupported."); + } + } + const admitted = omitPayloadIdentity(record); if ("run" in record && record["run"] !== undefined) { @@ -662,6 +725,21 @@ function readRunPayload( throw new Error("Runtime event run.failed payload must include an error."); } + if (context.kind === "run.failed") { + const recoverable = record["recoverable"]; + const error = admitted["error"] as RuntimeEventRecord; + + if (typeof recoverable !== "boolean") { + throw new Error("Runtime event run.failed payload recoverable must be a boolean."); + } + + if (error["retryable"] !== recoverable) { + throw new Error( + "Runtime event run.failed payload recoverable must agree with error.retryable.", + ); + } + } + return admitted; } @@ -674,7 +752,11 @@ function readRunView( value: unknown, ): RuntimeEventRecord { const record = requirePayloadRecord(context.kind, value, "run"); - requireEnumValue(record, "status", runStatuses, context.kind); + const status = requireEnumValue(record, "status", runStatuses, context.kind); + + if (!isRunStatusAllowedForKind(context.kind, status)) { + throw new Error(`Runtime event ${context.kind} payload run.status is inconsistent.`); + } return { completedAt: requireNullableTimestamp(record, "completedAt", context.kind, "run.completedAt"), @@ -682,34 +764,48 @@ function readRunView( record["error"] === null ? null : readRunError(context.kind, record["error"], "run.error"), id: context.runId ?? null, startedAt: requireNullableTimestamp(record, "startedAt", context.kind, "run.startedAt"), - status: record["status"], + status, traceId: context.traceId ?? null, }; } +function isRunStatusAllowedForKind(kind: RuntimeEventKind, status: string): boolean { + switch (kind) { + case "run.cancel.requested": + case "run.dispatched": + case "run.started": + case "run.steered": + case "run.waiting": + return status === "booting" || status === "running" || status === "waiting_input"; + case "run.cancelled": + return status === "cancelled" || status === "expired"; + case "run.completed": + return status === "completed"; + case "run.failed": + return status === "failed"; + case "run.queued": + return status === "queued"; + default: + return false; + } +} + function readRunError(kind: RuntimeEventKind, value: unknown, label: string): RuntimeEventRecord { const record = requirePayloadRecord(kind, value, label); - const details = record["details"]; - const recoverable = record["recoverable"]; const retryable = record["retryable"]; - if (details !== undefined && !isRuntimeEventRecord(details)) { - throw new Error(`Runtime event ${kind} payload ${label}.details must be an object.`); - } - - if (recoverable !== undefined && typeof recoverable !== "boolean") { - throw new Error(`Runtime event ${kind} payload ${label}.recoverable must be a boolean.`); - } - - if (retryable !== undefined && typeof retryable !== "boolean") { + if (typeof retryable !== "boolean") { throw new Error(`Runtime event ${kind} payload ${label}.retryable must be a boolean.`); } return { code: requireString(record, "code", kind), - details: readPrimitiveRecord(details), + details: readPrimitiveRecord( + record["details"], + `Runtime event ${kind} payload ${label}.details`, + ), message: requireString(record, "message", kind), - retryable: retryable === true || recoverable === true, + retryable, }; } diff --git a/src/protocol/orpc/index.ts b/src/protocol/orpc/index.ts index d790112..d235d22 100644 --- a/src/protocol/orpc/index.ts +++ b/src/protocol/orpc/index.ts @@ -1,17 +1,21 @@ import { z } from "zod"; import { + DURABLE_RUN_ERROR_MAX_UTF8_BYTES, DRIVER_CAPABILITY_IDS, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + measureRuntimeCommandJson, parseRuntimeCommand, - RUNTIME_COMMAND_STATUSES, } from "../../runtime-command"; import { DRIVER_PROTOCOL_VERSION } from "../boot"; -import { parseDriverEventEnvelope } from "../events"; +import { RUNTIME_EVENT_KINDS, parseDriverEventEnvelope } from "../events"; import { SUPPORTED_DRIVER_RUNTIMES } from "../runtime"; const nonEmptyStringSchema = z.string().min(1); +const lowercaseUuidV4Schema = z.uuidv4().regex(/^[0-9a-f-]+$/u, "UUID must be lowercase."); const primitiveSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); -const okSchema = z.object({ ok: z.literal(true) }); +const okSchema = z.strictObject({ ok: z.literal(true) }); +const driverRpcBatchMaxSize = 64; function omitUndefined>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T; @@ -80,7 +84,7 @@ function primitiveRecordSchema(label: string) { } const driverCapabilitySchema = z - .object({ + .strictObject({ details: z.string().optional(), id: z.enum(DRIVER_CAPABILITY_IDS, { error: "capability id is unsupported." }), status: z.enum(["supported", "unsupported"], { @@ -101,53 +105,49 @@ const driverCapabilitiesSchema = z } }); -const runErrorSchema = z.object({ - code: nonEmptyStringSchema, - details: primitiveRecordSchema("driver failure details"), - message: z.string(), - retryable: z.boolean(), -}); +const durableRunErrorSchema = z + .strictObject({ + code: nonEmptyStringSchema, + details: primitiveRecordSchema("driver failure details"), + message: nonEmptyStringSchema, + retryable: z.boolean(), + }) + .refine((error) => measureRuntimeCommandJson(error) <= DURABLE_RUN_ERROR_MAX_UTF8_BYTES, { + error: `Driver error must not exceed ${String(DURABLE_RUN_ERROR_MAX_UTF8_BYTES)} UTF-8 bytes.`, + }); -const inputStartCommandResultSchema = z.object({ +const inputStartCommandResultSchema = z.strictObject({ requestId: nonEmptyStringSchema, }); const mcpExecuteCommandResultSchema = z - .object({ + .strictObject({ isError: z.boolean().optional(), outputText: z.string(), requestId: nonEmptyStringSchema, serverId: nonEmptyStringSchema, toolName: nonEmptyStringSchema, }) - .transform(omitUndefined); - -const runtimeCommandResultSchema = z.unknown().transform((value, ctx) => { - if (value === null) { - return null; - } - - const record = value as Record; - const result = ( - record !== null && - typeof record === "object" && - !Array.isArray(record) && - ["isError", "outputText", "serverId", "toolName"].some((field) => Object.hasOwn(record, field)) - ? mcpExecuteCommandResultSchema - : inputStartCommandResultSchema - ).safeParse(value); - - if (result.success) { - return result.data; - } - - for (const issue of result.error.issues) { - ctx.addIssue({ code: "custom", message: issue.message, path: issue.path }); - } - return z.NEVER; -}); + .transform(omitUndefined) + .refine( + (result) => + measureRuntimeCommandJson(result) <= RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + { + error: `MCP command result must not exceed ${String(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES)} UTF-8 bytes.`, + }, + ); + +const runtimeCommandResultSchema = z + .union([inputStartCommandResultSchema, mcpExecuteCommandResultSchema]) + .refine( + (result) => + measureRuntimeCommandJson(result) <= RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + { + error: `Driver command result must not exceed ${String(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES)} UTF-8 bytes.`, + }, + ); -const driverHelloInputSchema = z.object({ +const driverHelloInputSchema = z.strictObject({ capabilities: driverCapabilitiesSchema, driverVersion: nonEmptyStringSchema, pid: positiveSafeIntegerSchema("pid"), @@ -158,39 +158,44 @@ const driverHelloInputSchema = z.object({ startedAt: nonEmptyStringSchema, }); -const driverHelloOutputSchema = z.object({ +const driverHelloOutputSchema = z.strictObject({ acceptedCapabilities: driverCapabilitiesSchema, connectionId: nonEmptyStringSchema, driverInstanceId: nonEmptyStringSchema, heartbeatIntervalMs: positiveSafeIntegerSchema("Driver heartbeat interval", 250), - runConfig: z.object({ - commandLeaseMs: z.number().nonnegative(), + runConfig: z.strictObject({ + commandLeaseMs: nonNegativeSafeIntegerSchema("Driver command lease"), envPolicy: z.literal("strict"), - eventBatchMaxSize: positiveSafeIntegerSchema("Driver event batch max size"), + eventBatchMaxSize: positiveSafeIntegerSchema("Driver event batch max size").refine( + (value) => value <= driverRpcBatchMaxSize, + { + error: `Driver event batch max size must not exceed ${String(driverRpcBatchMaxSize)}.`, + }, + ), organizationPath: nonEmptyStringSchema, }), runId: nonEmptyStringSchema.nullable(), }); -const driverHeartbeatInputSchema = z.object({ +const driverHeartbeatInputSchema = z.strictObject({ at: nonEmptyStringSchema, pid: positiveSafeIntegerSchema("pid"), reason: z.enum(["interval", "ping"], { error: "reason must be interval or ping." }), }); -const driverHeartbeatOutputSchema = z.object({ +const driverHeartbeatOutputSchema = z.strictObject({ heartbeatCount: nonNegativeSafeIntegerSchema("heartbeatCount"), ok: z.literal(true), }); -const driverReadyInputSchema = z.object({ +const driverReadyInputSchema = z.strictObject({ at: nonEmptyStringSchema, driverInstanceId: nonEmptyStringSchema, pid: positiveSafeIntegerSchema("pid"), }); const driverLogContextSchema = z - .object({ + .strictObject({ parentSpanId: z.string().optional(), requestId: z.string().optional(), sandboxId: z.string().optional(), @@ -201,7 +206,7 @@ const driverLogContextSchema = z .transform(omitUndefined); const driverLogErrorSchema = z - .object({ + .strictObject({ code: z.union([z.string(), z.number()]).optional(), message: z.string(), name: z.string(), @@ -210,7 +215,7 @@ const driverLogErrorSchema = z .transform(omitUndefined); const driverLogEntrySchema = z - .object({ + .strictObject({ context: driverLogContextSchema.optional(), error: driverLogErrorSchema.optional(), fields: primitiveRecordSchema("driver log fields").optional(), @@ -224,87 +229,135 @@ const driverLogEntrySchema = z }) .transform(omitUndefined); -const driverLogBatchInputSchema = z.object({ +const driverLogBatchInputSchema = z.strictObject({ driverInstanceId: nonEmptyStringSchema, - logs: z.array(driverLogEntrySchema), + logs: z.array(driverLogEntrySchema).max(driverRpcBatchMaxSize), }); -const driverFailureInputSchema = z.object({ +const driverFailureInputSchema = z.strictObject({ driverInstanceId: nonEmptyStringSchema, - error: runErrorSchema, + error: durableRunErrorSchema, + runId: nonEmptyStringSchema, }); -const driverCommandUpdateInputSchema = z - .object({ - commandId: nonEmptyStringSchema, - driverInstanceId: nonEmptyStringSchema, - error: runErrorSchema.optional(), +const driverCommandUpdateIdentitySchema = { + commandId: nonEmptyStringSchema, + driverInstanceId: nonEmptyStringSchema, +} as const; +const driverCommandUpdateInputSchema = z.discriminatedUnion("status", [ + z.strictObject({ ...driverCommandUpdateIdentitySchema, status: z.literal("accepted") }), + z.strictObject({ ...driverCommandUpdateIdentitySchema, status: z.literal("cancelled") }), + z.strictObject({ + ...driverCommandUpdateIdentitySchema, result: runtimeCommandResultSchema.optional(), - status: z.enum(RUNTIME_COMMAND_STATUSES, { - error: "status is not a supported runtime command status.", - }), - }) - .transform(omitUndefined); + status: z.literal("completed"), + }), + z.strictObject({ + ...driverCommandUpdateIdentitySchema, + error: durableRunErrorSchema, + status: z.literal("failed"), + }), +]); -const driverExternalToolEffectClaimInputSchema = z.object({ +const driverExternalToolEffectObserveInputSchema = z.strictObject({ + commandId: nonEmptyStringSchema, + driverInstanceId: nonEmptyStringSchema, +}); + +const driverExternalToolEffectIntentSchema = z.strictObject({ + effectId: nonEmptyStringSchema, + kind: z.literal("intent"), +}); + +const driverExternalToolEffectClaimedSchema = z.strictObject({ + attempt: positiveSafeIntegerSchema("attempt"), + effectId: nonEmptyStringSchema, + idempotencyKey: nonEmptyStringSchema, + kind: z.literal("claimed"), +}); + +const driverExternalToolEffectSucceededSchema = z.strictObject({ + effectId: nonEmptyStringSchema, + kind: z.literal("succeeded"), + result: mcpExecuteCommandResultSchema, +}); + +const driverExternalToolEffectUnknownSchema = z.strictObject({ + effectId: nonEmptyStringSchema, + kind: z.literal("unknown"), +}); + +const driverExternalToolEffectStateSchema = z.discriminatedUnion("kind", [ + driverExternalToolEffectIntentSchema, + driverExternalToolEffectClaimedSchema, + driverExternalToolEffectSucceededSchema, + driverExternalToolEffectUnknownSchema, +]); + +const driverExternalToolEffectClaimInputSchema = z.strictObject({ + claimToken: lowercaseUuidV4Schema, commandId: nonEmptyStringSchema, driverInstanceId: nonEmptyStringSchema, }); const driverExternalToolEffectClaimOutputSchema = z.discriminatedUnion("kind", [ - z.object({ - attempt: positiveSafeIntegerSchema("attempt"), - effectId: nonEmptyStringSchema, - idempotencyKey: nonEmptyStringSchema, - kind: z.literal("execute"), - }), - z.object({ - effectId: nonEmptyStringSchema, - kind: z.literal("completed"), - result: mcpExecuteCommandResultSchema, - }), - z.object({ - effectId: nonEmptyStringSchema, - kind: z.literal("unknown"), - }), + driverExternalToolEffectClaimedSchema, + driverExternalToolEffectSucceededSchema, + driverExternalToolEffectUnknownSchema, ]); -const driverExternalToolEffectCompleteInputSchema = z - .object({ - commandId: nonEmptyStringSchema, - driverInstanceId: nonEmptyStringSchema, - providerReceiptJson: z.string().nullable().optional(), - result: mcpExecuteCommandResultSchema, - }) - .transform(omitUndefined); +const driverExternalToolEffectSettlementSchema = z + .discriminatedUnion("kind", [ + z.strictObject({ + kind: z.literal("succeeded"), + providerReceiptJson: z.string().nullable().optional(), + result: mcpExecuteCommandResultSchema, + }), + z.strictObject({ kind: z.literal("unknown") }), + ]) + .refine( + (settlement) => + measureRuntimeCommandJson(settlement) <= RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + { + error: `MCP settlement must not exceed ${String(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES)} UTF-8 bytes.`, + }, + ); -const driverExternalToolEffectUnknownInputSchema = z.object({ +const driverExternalToolEffectSettleInputSchema = z.strictObject({ + claimToken: lowercaseUuidV4Schema, commandId: nonEmptyStringSchema, driverInstanceId: nonEmptyStringSchema, + effectId: nonEmptyStringSchema, + settlement: driverExternalToolEffectSettlementSchema, }); -const driverEventBatchInputSchema = z.object({ +const driverEventBatchInputSchema = z.strictObject({ driverInstanceId: nonEmptyStringSchema, - events: z.array(parsedSchema(parseDriverEventEnvelope)), + events: z.array(parsedSchema(parseDriverEventEnvelope)).max(driverRpcBatchMaxSize), }); const driverEventReceiptSchema = z - .object({ - eventId: z.string().optional(), + .strictObject({ + eventId: nonEmptyStringSchema, seq: nonNegativeSafeIntegerSchema("Driver event receipt seq"), - type: z.string(), + type: z.enum(RUNTIME_EVENT_KINDS), }) .transform(omitUndefined); -const driverEventBatchOutputSchema = z.object({ +const driverEventBatchOutputSchema = z.strictObject({ accepted: z.array(driverEventReceiptSchema), }); -const driverInstanceInputSchema = z.object({ +const driverInstanceInputSchema = z.strictObject({ driverInstanceId: nonEmptyStringSchema, }); -const driverNextCommandOutputSchema = z.object({ +const driverCompletionInputSchema = z.strictObject({ + driverInstanceId: nonEmptyStringSchema, + runId: nonEmptyStringSchema, +}); + +const driverNextCommandOutputSchema = z.strictObject({ command: parsedSchema(parseRuntimeCommand).nullable(), }); @@ -334,22 +387,22 @@ type SchemaValue = DeepReadonly>; export const driverRuntimeRpcSchemas = { driver: { + observeExternalToolEffect: { + input: driverExternalToolEffectObserveInputSchema, + output: driverExternalToolEffectStateSchema, + }, claimExternalToolEffect: { input: driverExternalToolEffectClaimInputSchema, output: driverExternalToolEffectClaimOutputSchema, }, commandUpdate: { input: driverCommandUpdateInputSchema, output: okSchema }, - completeExternalToolEffect: { - input: driverExternalToolEffectCompleteInputSchema, - output: okSchema, - }, - completeRun: { input: driverInstanceInputSchema, output: okSchema }, + completeRun: { input: driverCompletionInputSchema, output: okSchema }, failRun: { input: driverFailureInputSchema, output: okSchema }, heartbeat: { input: driverHeartbeatInputSchema, output: driverHeartbeatOutputSchema }, hello: { input: driverHelloInputSchema, output: driverHelloOutputSchema }, - markExternalToolEffectUnknown: { - input: driverExternalToolEffectUnknownInputSchema, - output: okSchema, + settleExternalToolEffect: { + input: driverExternalToolEffectSettleInputSchema, + output: driverExternalToolEffectStateSchema, }, pushEvents: { input: driverEventBatchInputSchema, output: driverEventBatchOutputSchema }, pushLogs: { input: driverLogBatchInputSchema, output: okSchema }, @@ -392,24 +445,25 @@ export type DriverLogBatchInput = Omit; export type DriverFailureInput = SchemaValue; export type DriverCommandUpdateInput = SchemaValue; +export type DriverExternalToolEffectObserveInput = SchemaValue< + typeof driverExternalToolEffectObserveInputSchema +>; +export type DriverExternalToolEffectState = SchemaValue; export type DriverExternalToolEffectClaimInput = SchemaValue< typeof driverExternalToolEffectClaimInputSchema >; export type DriverExternalToolEffectClaimOutput = SchemaValue< typeof driverExternalToolEffectClaimOutputSchema >; -export type DriverExternalToolEffectCompleteInput = SchemaValue< - typeof driverExternalToolEffectCompleteInputSchema ->; -export type DriverExternalToolEffectUnknownInput = SchemaValue< - typeof driverExternalToolEffectUnknownInputSchema +export type DriverExternalToolEffectSettleInput = SchemaValue< + typeof driverExternalToolEffectSettleInputSchema >; export type DriverEventBatchInput = SchemaValue; export type DriverEventReceipt = SchemaValue; export type DriverEventBatchOutput = SchemaValue; export type DriverNextCommandInput = SchemaValue; export type DriverNextCommandOutput = SchemaValue; -export type DriverCompletionInput = SchemaValue; +export type DriverCompletionInput = SchemaValue; export function parseDriverHelloInput(value: unknown): DriverHelloInput { return driverHelloInputSchema.parse(value); @@ -427,26 +481,26 @@ export function parseDriverCommandUpdateInput(value: unknown): DriverCommandUpda return driverCommandUpdateInputSchema.parse(value); } -export function parseDriverExternalToolEffectClaimInput( +export function parseDriverExternalToolEffectObserveInput( value: unknown, -): DriverExternalToolEffectClaimInput { - return driverExternalToolEffectClaimInputSchema.parse(value); +): DriverExternalToolEffectObserveInput { + return driverExternalToolEffectObserveInputSchema.parse(value); } -export function parseDriverExternalToolEffectCompleteInput( +export function parseDriverExternalToolEffectClaimInput( value: unknown, -): DriverExternalToolEffectCompleteInput { - return driverExternalToolEffectCompleteInputSchema.parse(value); +): DriverExternalToolEffectClaimInput { + return driverExternalToolEffectClaimInputSchema.parse(value); } -export function parseDriverExternalToolEffectUnknownInput( +export function parseDriverExternalToolEffectSettleInput( value: unknown, -): DriverExternalToolEffectUnknownInput { - return driverExternalToolEffectUnknownInputSchema.parse(value); +): DriverExternalToolEffectSettleInput { + return driverExternalToolEffectSettleInputSchema.parse(value); } export function parseDriverCompletionInput(value: unknown): DriverCompletionInput { - return driverInstanceInputSchema.parse(value); + return driverCompletionInputSchema.parse(value); } export function parseDriverFailureInput(value: unknown): DriverFailureInput { diff --git a/src/runtime-command/index.ts b/src/runtime-command/index.ts index 176cbe2..9d6d356 100644 --- a/src/runtime-command/index.ts +++ b/src/runtime-command/index.ts @@ -1,6 +1,22 @@ export type PrimitiveValue = string | number | boolean | null; export type PrimitiveRecord = Record; +const D1_TABLE_ROW_MAX_UTF8_BYTES = 2_000_000; +const RUNTIME_COMMAND_ROW_RESERVED_UTF8_BYTES = 128 * 1_024; + +export const DURABLE_RUN_ERROR_MAX_UTF8_BYTES = 1_020 * 1_024; +export const RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES = DURABLE_RUN_ERROR_MAX_UTF8_BYTES; +export const RUNTIME_COMMAND_MAX_UTF8_BYTES = + D1_TABLE_ROW_MAX_UTF8_BYTES - + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES - + RUNTIME_COMMAND_ROW_RESERVED_UTF8_BYTES; + +const runtimeCommandEncoder = new TextEncoder(); + +export function measureRuntimeCommandJson(value: unknown): number { + return runtimeCommandEncoder.encode(JSON.stringify(value)).byteLength; +} + export interface RunError { readonly code: string; readonly details: PrimitiveRecord; @@ -20,6 +36,7 @@ export const RUNTIME_COMMAND_STATUSES = [ export type RuntimeCommandStatus = (typeof RUNTIME_COMMAND_STATUSES)[number]; export interface RuntimeCommandInput { + readonly attachmentIds?: readonly string[] | undefined; readonly text: string; } @@ -27,6 +44,7 @@ export interface TurnCancelCommand { readonly commandId: string; readonly kind: "turn.cancel"; readonly reason?: string | undefined; + readonly runId: string; } export interface InputStartCommand { @@ -48,16 +66,54 @@ export interface McpExecuteCommand { readonly commandId: string; readonly kind: "mcp.execute"; readonly requestId: string; + readonly runId: string; readonly serverId: string; readonly toolCallId: string; readonly toolName: string; } +type McpEffectErrorCommand = Pick< + McpExecuteCommand, + "commandId" | "requestId" | "runId" | "serverId" | "toolName" +>; + +export function createMcpUnknownEffectRunError( + command: McpEffectErrorCommand, + effectId: string, +): RunError { + return { + code: "driver.external_tool_effect_unknown", + details: { + commandId: command.commandId, + effectId, + requestId: command.requestId, + runId: command.runId, + serverId: command.serverId, + toolName: command.toolName, + }, + message: `External effect ${effectId} for MCP tool ${command.toolName} has an unknown outcome and will not be replayed.`, + retryable: false, + }; +} + +export function createMcpUnsettledEffectRunError( + command: Pick, + effectId: string, +): RunError { + return { + code: "driver.command_failed.mcp.execute", + details: { commandId: command.commandId, commandKind: "mcp.execute" }, + message: `External effect ${effectId} for MCP tool ${command.toolName} requires server-side repair.`, + retryable: false, + }; +} + export interface PermissionResolveCommand { readonly commandId: string; readonly decision: "allow_once" | "reject_once"; readonly kind: "permission.resolve"; readonly requestId: string; + readonly runId: string; } export type RuntimeCommand = @@ -87,26 +143,22 @@ export interface McpExternalToolExecutionResult extends McpExecuteCommandResult readonly providerReceiptJson?: string | null | undefined; } -/** - * The API-owned decision made immediately before the Driver invokes an - * external MCP provider. The key survives Driver replacement and is conveyed - * to providers through MCP request metadata. - */ export interface McpExternalToolEffectExecution { + readonly attempt: number; readonly effectId: string; readonly idempotencyKey: string; + readonly kind: "claimed"; } -export type McpExternalToolEffectClaim = +export type McpExternalToolEffectState = | { - readonly attempt: number; readonly effectId: string; - readonly idempotencyKey: string; - readonly kind: "execute"; + readonly kind: "intent"; } + | McpExternalToolEffectExecution | { readonly effectId: string; - readonly kind: "completed"; + readonly kind: "succeeded"; readonly result: McpExecuteCommandResult; } | { @@ -114,8 +166,37 @@ export type McpExternalToolEffectClaim = readonly kind: "unknown"; }; +export type McpExternalToolEffectClaim = Exclude< + McpExternalToolEffectState, + { readonly kind: "intent" } +>; + +export type McpExternalToolEffectSettlement = + | { + readonly kind: "succeeded"; + readonly providerReceiptJson?: string | null | undefined; + readonly result: McpExecuteCommandResult; + } + | { readonly kind: "unknown" }; + export type RuntimeCommandResult = InputStartCommandResult | McpExecuteCommandResult | null; +export type DriverCommandUpdate = + | { + readonly commandId: string; + readonly status: "accepted" | "cancelled"; + } + | { + readonly commandId: string; + readonly result?: Exclude | undefined; + readonly status: "completed"; + } + | { + readonly commandId: string; + readonly error: RunError; + readonly status: "failed"; + }; + export const DRIVER_CAPABILITY_IDS = [ "custom_tool_execute", "file_change", @@ -133,8 +214,6 @@ export const DRIVER_CAPABILITY_IDS = [ ] as const; export type DriverCapabilityId = (typeof DRIVER_CAPABILITY_IDS)[number]; -const MAX_RUNTIME_CONTROL_REASON_BYTES = 16 * 1_024; - export interface DriverCapability { readonly details?: string | undefined; readonly id: DriverCapabilityId; @@ -154,6 +233,19 @@ function readRecord(value: unknown, label: string): Record { return value; } +function requireExactKeys( + record: Record, + allowedKeys: readonly string[], + label: string, +): void { + const allowed = new Set(allowedKeys); + const unexpected = Object.keys(record).find((key) => !allowed.has(key)); + + if (unexpected !== undefined) { + throw new TypeError(`${label}.${unexpected} is not allowed.`); + } +} + function readNonEmptyString(record: Record, field: string): string { const value = record[field]; @@ -188,28 +280,52 @@ function readOptionalString(record: Record, field: string): str return value; } -function normalizeControlReason(reason: string): string { - const bytes = new TextEncoder().encode(reason).byteLength; - return bytes <= MAX_RUNTIME_CONTROL_REASON_BYTES - ? reason - : `Runtime command reason exceeded ${String(MAX_RUNTIME_CONTROL_REASON_BYTES)} UTF-8 bytes (received ${String(bytes)}).`; +export function normalizeDurableRunError(error: RunError): RunError { + const byteLength = measureRuntimeCommandJson(error); + + return byteLength <= DURABLE_RUN_ERROR_MAX_UTF8_BYTES + ? error + : { + code: "driver.error_oversized", + details: { originalBytes: byteLength }, + message: `Driver error exceeded ${String(DURABLE_RUN_ERROR_MAX_UTF8_BYTES)} UTF-8 bytes and was omitted.`, + retryable: false, + }; +} + +function requireDurableRuntimeCommand( + command: Command, + storedCommand: unknown = command, +): Command { + const byteLength = measureRuntimeCommandJson(storedCommand); + + if (byteLength > RUNTIME_COMMAND_MAX_UTF8_BYTES) { + throw new RangeError( + `Runtime command exceeds ${String(RUNTIME_COMMAND_MAX_UTF8_BYTES)} UTF-8 bytes.`, + ); + } + + return command; } function readRuntimeCommandInput(value: unknown): RuntimeCommandInput { const record = readRecord(value, "input"); + requireExactKeys(record, ["attachmentIds", "text"], "input"); const attachmentIds = record["attachmentIds"]; + const text = readNonEmptyString(record, "text"); + + if (attachmentIds === undefined) { + return { text }; + } if ( - attachmentIds !== undefined && - (!Array.isArray(attachmentIds) || - attachmentIds.some((id) => typeof id !== "string" || id.length === 0)) + !Array.isArray(attachmentIds) || + attachmentIds.some((id) => typeof id !== "string" || id.length === 0) ) { throw new TypeError("input.attachmentIds must be an array of non-empty strings."); } - return { - text: readNonEmptyString(record, "text"), - }; + return { attachmentIds: [...attachmentIds], text }; } function readPermissionDecision(value: unknown): PermissionResolveCommand["decision"] { @@ -226,45 +342,77 @@ export function parseRuntimeCommand(value: unknown): RuntimeCommand { switch (kind) { case "input.start": { - return { + requireExactKeys( + record, + ["commandId", "input", "kind", "requestId", "runId"], + "runtime command", + ); + const input = readRuntimeCommandInput(record["input"]); + const command = { commandId: readNonEmptyString(record, "commandId"), - input: readRuntimeCommandInput(record["input"]), + input, kind, requestId: readNonEmptyString(record, "requestId"), runId: readNonEmptyString(record, "runId"), }; + + return requireDurableRuntimeCommand(command); } case "mcp.execute": - return { + requireExactKeys( + record, + [ + "argumentsJson", + "commandId", + "kind", + "requestId", + "runId", + "serverId", + "toolCallId", + "toolName", + ], + "runtime command", + ); + return requireDurableRuntimeCommand({ argumentsJson: readString(record, "argumentsJson"), commandId: readNonEmptyString(record, "commandId"), kind, requestId: readNonEmptyString(record, "requestId"), + runId: readNonEmptyString(record, "runId"), serverId: readNonEmptyString(record, "serverId"), toolCallId: readNonEmptyString(record, "toolCallId"), toolName: readNonEmptyString(record, "toolName"), - }; + }); case "permission.resolve": - return { + requireExactKeys( + record, + ["commandId", "decision", "kind", "requestId", "runId"], + "runtime command", + ); + return requireDurableRuntimeCommand({ commandId: readNonEmptyString(record, "commandId"), decision: readPermissionDecision(record["decision"]), kind, requestId: readNonEmptyString(record, "requestId"), - }; + runId: readNonEmptyString(record, "runId"), + }); case "session.stop": - return { + requireExactKeys(record, ["commandId", "kind", "reason"], "runtime command"); + return requireDurableRuntimeCommand({ commandId: readNonEmptyString(record, "commandId"), kind, - reason: normalizeControlReason(readNonEmptyString(record, "reason")), - }; + reason: readNonEmptyString(record, "reason"), + }); case "turn.cancel": { + requireExactKeys(record, ["commandId", "kind", "reason", "runId"], "runtime command"); const reason = readOptionalString(record, "reason"); - return { + return requireDurableRuntimeCommand({ commandId: readNonEmptyString(record, "commandId"), kind, - ...(reason === undefined ? {} : { reason: normalizeControlReason(reason) }), - }; + ...(reason === undefined ? {} : { reason }), + runId: readNonEmptyString(record, "runId"), + }); } default: throw new TypeError(`Unsupported runtime command kind: ${kind}.`); diff --git a/src/runtimes/acp/acp-assistant-transcript-state.ts b/src/runtimes/acp/acp-assistant-transcript-state.ts index ccad62b..8254f04 100644 --- a/src/runtimes/acp/acp-assistant-transcript-state.ts +++ b/src/runtimes/acp/acp-assistant-transcript-state.ts @@ -216,6 +216,7 @@ export class AcpAssistantTranscriptState { error: { code: `acp.${stopReason}`, message: terminalError, + retryable: false, }, recoverable: false, stopReason, @@ -229,6 +230,7 @@ export class AcpAssistantTranscriptState { error: { code: "acp.empty_turn", message: "ACP prompt ended without assistant output or tool activity.", + retryable: true, }, recoverable: true, stopReason, @@ -239,7 +241,7 @@ export class AcpAssistantTranscriptState { events.push({ kind: "run.completed", payload: { - ...(finalMessage === null + ...(finalMessage === null || !finalMessage.hasVisibleText ? {} : { finalMessageId: finalMessage.id, @@ -298,6 +300,7 @@ export class AcpAssistantTranscriptState { code: error.code, ...(message === error.message ? {} : { details: { originalMessageUtf8Bytes } }), message, + retryable: error.recoverable ?? false, }, recoverable: error.recoverable ?? false, }, diff --git a/src/runtimes/acp/acp-client-request-handler.ts b/src/runtimes/acp/acp-client-request-handler.ts index 293eb47..14225f0 100644 --- a/src/runtimes/acp/acp-client-request-handler.ts +++ b/src/runtimes/acp/acp-client-request-handler.ts @@ -44,7 +44,18 @@ interface AcpClientRequestHandlerOptions { readonly turnEvents: AcpAssistantTranscriptState; } +interface AcpFileWrite { + failure: { readonly error: unknown } | null; + readonly task: Promise; +} + +interface AcpTurnFileWrites { + ingressClosed: boolean; + readonly writes: Set; +} + export class AcpClientRequestHandler { + readonly #activeFileWrites = new Set(); readonly #fileSystem: AcpFileSystem; readonly #isCancelling: () => boolean; readonly #nativeSessionId: () => string | null; @@ -55,6 +66,7 @@ export class AcpClientRequestHandler { #permissionIngressClosed = false; #stopping = false; #transcriptTail: Promise = Promise.resolve(); + #turnFileWrites: AcpTurnFileWrites | null = null; #turnTranscriptIngressClosed = false; readonly #updateInbox: AcpSessionUpdateInbox; readonly #terminalManager: AcpTerminalManager; @@ -122,7 +134,73 @@ export class AcpClientRequestHandler { signal?: AbortSignal, ): Promise { this.#assertSession("fs/write_text_file", params); - return this.#fileSystem.writeTextFile(context, params, signal); + if (this.#stopping) { + throw new Error("ACP client request handler is stopping."); + } + const turn = this.#turnFileWrites; + if (turn?.ingressClosed) { + throw new Error("ACP file write ingress is closed for the active turn."); + } + + let committed = false; + const operation = this.#fileSystem.writeTextFile(context, params, signal, () => { + committed = true; + }); + let write!: AcpFileWrite; + const task = operation.catch((error: unknown) => { + if (committed) { + write.failure = { error }; + this.#onUpdateFailure( + error instanceof Error + ? error + : new Error("ACP committed file report failed.", { cause: error }), + ); + } + throw error; + }); + write = { failure: null, task }; + this.#activeFileWrites.add(write); + turn?.writes.add(write); + + return task; + } + + beginStop(): void { + this.#stopping = true; + this.closeFileWriteIngress(); + } + + async drainFileWrites(signal?: AbortSignal): Promise { + await this.#drainFileWrites(this.#activeFileWrites, signal); + } + + openFileWriteIngress(): void { + if (this.#stopping) { + throw new Error("ACP client request handler is stopping."); + } + if ( + this.#turnFileWrites !== null && + (!this.#turnFileWrites.ingressClosed || this.#turnFileWrites.writes.size > 0) + ) { + throw new Error("ACP previous turn file writes have not drained."); + } + + this.#turnFileWrites = { ingressClosed: false, writes: new Set() }; + } + + closeFileWriteIngress(): void { + if (this.#turnFileWrites !== null) { + this.#turnFileWrites.ingressClosed = true; + } + } + + async drainTurnFileWrites(signal?: AbortSignal): Promise { + const turn = this.#turnFileWrites; + if (turn === null) { + return; + } + + await this.#drainFileWrites(turn.writes, signal); } async requestPermission( @@ -292,6 +370,23 @@ export class AcpClientRequestHandler { ); } + async #drainFileWrites(writes: Set, signal?: AbortSignal): Promise { + while (writes.size > 0) { + const pending = [...writes]; + await raceWithAbort(Promise.allSettled(pending.map(({ task }) => task)), signal); + for (const write of pending) { + writes.delete(write); + this.#activeFileWrites.delete(write); + this.#turnFileWrites?.writes.delete(write); + } + const failure = pending.find(({ failure }) => failure !== null)?.failure; + + if (failure !== null && failure !== undefined) { + throw failure.error; + } + } + } + async #requestPermission( context: AgentDriverContext, requestId: string, diff --git a/src/runtimes/acp/acp-driver-backend.ts b/src/runtimes/acp/acp-driver-backend.ts index 2f14d3d..978d53e 100644 --- a/src/runtimes/acp/acp-driver-backend.ts +++ b/src/runtimes/acp/acp-driver-backend.ts @@ -57,6 +57,7 @@ const ACP_STOP_BUDGET_MS = 4_000; const ACP_RECYCLE_BUDGET_MS = 4_500; const ACP_SESSION_SHUTDOWN_TIMEOUT_MS = 750; const ACP_UPDATE_DRAIN_TIMEOUT_MS = 750; +const MAX_ACTIVE_ACP_CLIENT_REQUESTS = 8; function toErrorMessage(error: unknown, fallback: string): string { return error instanceof Error ? error.message : fallback; @@ -220,7 +221,39 @@ export class AcpDriverBackend implements AgentDriverBackend { ); const agentProcess = startedProcess.process; this.#agentProcess = agentProcess; + let activeClientRequests = 0; let connection: ClientConnection | null = null; + const serveRequest = async ( + requestSignal: AbortSignal, + operation: (signal: AbortSignal) => Promise | T, + ): Promise => { + const signal = this.#requestSignal(requestSignal); + + if (this.#stopRequested || signal.aborted) { + throw RequestError.requestCancelled(); + } + if (activeClientRequests >= MAX_ACTIVE_ACP_CLIENT_REQUESTS) { + const error = RequestError.internalError( + { maxActiveRequests: MAX_ACTIVE_ACP_CLIENT_REQUESTS }, + "ACP client request capacity exceeded", + ); + connection?.close(error); + throw error; + } + activeClientRequests += 1; + + try { + return await operation(signal); + } catch (error) { + if (signal.aborted) { + throw RequestError.requestCancelled(); + } + + throw error; + } finally { + activeClientRequests -= 1; + } + }; try { await startedProcess.ready; @@ -234,42 +267,42 @@ export class AcpDriverBackend implements AgentDriverBackend { this.#clientRequests.enqueueUpdate(context, params), ) .onRequest(acpMethods.client.session.requestPermission, ({ params, requestId, signal }) => - this.#serveRequest(signal, (requestSignal) => + serveRequest(signal, (requestSignal) => this.#clientRequests.requestPermission(context, requestId, params, requestSignal), ), ) .onRequest(acpMethods.client.fs.readTextFile, ({ params, signal }) => - this.#serveRequest(signal, (requestSignal) => + serveRequest(signal, (requestSignal) => this.#clientRequests.readTextFile(params, requestSignal), ), ) .onRequest(acpMethods.client.fs.writeTextFile, ({ params, signal }) => - this.#serveRequest(signal, (requestSignal) => + serveRequest(signal, (requestSignal) => this.#clientRequests.writeTextFile(context, params, requestSignal), ), ) .onRequest(acpMethods.client.terminal.create, ({ params, signal }) => - this.#serveRequest(signal, (requestSignal) => + serveRequest(signal, (requestSignal) => this.#clientRequests.createTerminal(context, params, requestSignal), ), ) .onRequest(acpMethods.client.terminal.kill, ({ params, signal }) => - this.#serveRequest(signal, (requestSignal) => + serveRequest(signal, (requestSignal) => this.#clientRequests.killTerminal(context, params, requestSignal), ), ) .onRequest(acpMethods.client.terminal.output, ({ params, signal }) => - this.#serveRequest(signal, (requestSignal) => + serveRequest(signal, (requestSignal) => this.#clientRequests.terminalOutput(params, requestSignal), ), ) .onRequest(acpMethods.client.terminal.release, ({ params, signal }) => - this.#serveRequest(signal, (requestSignal) => + serveRequest(signal, (requestSignal) => this.#clientRequests.releaseTerminal(context, params, requestSignal), ), ) .onRequest(acpMethods.client.terminal.waitForExit, ({ params, signal }) => - this.#serveRequest(signal, (requestSignal) => + serveRequest(signal, (requestSignal) => this.#clientRequests.waitForTerminalExit(params, requestSignal), ), ); @@ -451,10 +484,11 @@ export class AcpDriverBackend implements AgentDriverBackend { .finally(() => this.#clientRequests.closePathScope()); const task = operation .then(() => { + this.#stopFailure = null; this.#stopSucceeded = true; }) .catch((error: unknown) => { - this.#stopFailure ??= { error }; + this.#stopFailure = { error }; throw error; }) .finally(() => { @@ -525,6 +559,12 @@ export class AcpDriverBackend implements AgentDriverBackend { ): Promise { const deadline = Date.now() + ACP_STOP_BUDGET_MS; this.#turnController.abort("ACP driver backend stopped."); + this.#clientRequests.beginStop(); + const fileWriteDrainTask = settlePromiseWithTimeout(this.#clientRequests.drainFileWrites(), { + label: "ACP committed file report drain", + signal, + timeoutMs: this.#remainingStopMs(deadline), + }); const terminalCleanupTask = settlePromiseWithTimeout( this.#clientRequests.stopTerminals(context), { @@ -588,6 +628,15 @@ export class AcpDriverBackend implements AgentDriverBackend { }); } + const fileWriteDrain = await fileWriteDrainTask; + + if (fileWriteDrain.status !== "completed") { + context.logger.warn("driver.acp.files.drain.failed", { + message: toErrorMessage(fileWriteDrain.error, "committed file report drain failed"), + reason, + }); + } + transport?.close(new Error("ACP driver backend stopped.")); if (this.#connection === transport) { this.#connection = null; @@ -620,6 +669,10 @@ export class AcpDriverBackend implements AgentDriverBackend { throw updateDrain.error; } + if (fileWriteDrain.status !== "completed") { + throw fileWriteDrain.error; + } + if (terminalCleanup.status !== "completed") { throw terminalCleanup.error; } @@ -714,9 +767,6 @@ export class AcpDriverBackend implements AgentDriverBackend { } async #joinStop(): Promise { - if (this.#stopFailure !== null) { - throw this.#stopFailure.error; - } if (this.#stopTask !== null) { await this.#stopTask; return; @@ -724,6 +774,9 @@ export class AcpDriverBackend implements AgentDriverBackend { if (this.#stopSucceeded) { return; } + if (this.#stopFailure !== null) { + throw this.#stopFailure.error; + } throw new Error("ACP driver stop was requested without an owned cleanup barrier."); } @@ -767,23 +820,6 @@ export class AcpDriverBackend implements AgentDriverBackend { return turnSignal === undefined ? signal : AbortSignal.any([signal, turnSignal]); } - async #serveRequest( - signal: AbortSignal, - operation: (signal: AbortSignal) => Promise | T, - ): Promise { - const requestSignal = this.#requestSignal(signal); - - try { - return await operation(requestSignal); - } catch (error) { - if (requestSignal.aborted) { - throw RequestError.requestCancelled(); - } - - throw error; - } - } - async #setupSession(signal: AbortSignal): Promise>> { signal.throwIfAborted(); const deferredUpdates = diff --git a/src/runtimes/acp/acp-file-system.ts b/src/runtimes/acp/acp-file-system.ts index 3f2266c..e840508 100644 --- a/src/runtimes/acp/acp-file-system.ts +++ b/src/runtimes/acp/acp-file-system.ts @@ -5,7 +5,8 @@ import { lstat, open, rename, rm } from "node:fs/promises"; import { join } from "node:path"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; -import { isRecord, readNonEmptyString, readNumber } from "./acp-types"; +import { DRIVER_EVENT_DELIVERY_TIMEOUT_MS } from "../../core/driver-runtime-io"; +import { isRecord, raceWithAbort, readNonEmptyString, readNumber } from "./acp-types"; import { AcpPathScope } from "./acp-path-scope"; interface AcpFileSystemOptions { @@ -72,6 +73,7 @@ export class AcpFileSystem { context: AgentDriverContext, params: unknown, signal?: AbortSignal, + onCommitted?: () => void, ): Promise> { signal?.throwIfAborted(); const record = isRecord(params) ? params : {}; @@ -91,6 +93,7 @@ export class AcpFileSystem { const destinationPath = join(path.directory.procPath, path.name); let temporaryCreated = false; let committed = false; + let changedPath: string; try { const mode = await readExistingMode(destinationPath); @@ -126,17 +129,13 @@ export class AcpFileSystem { signal?.throwIfAborted(); await rename(temporaryPath, destinationPath); committed = true; + onCommitted?.(); await path.directory.file.sync(); } finally { await temporary.close(); } - const changedPath = join(await this.#pathScope.identify(path.directory), path.name); - await context.ports.file.reportChanged({ - change: "upsert", - path: changedPath, - reason: "acp.fs", - }); + changedPath = join(await this.#pathScope.identify(path.directory), path.name); } finally { try { if (temporaryCreated && !committed) { @@ -147,6 +146,19 @@ export class AcpFileSystem { } } + const reportSignal = AbortSignal.timeout(DRIVER_EVENT_DELIVERY_TIMEOUT_MS); + await raceWithAbort( + context.ports.file.reportChanged( + { + change: "upsert", + path: changedPath, + reason: "acp.fs", + }, + reportSignal, + ), + reportSignal, + ); + return {}; } } diff --git a/src/runtimes/acp/acp-permission-events.ts b/src/runtimes/acp/acp-permission-events.ts index 98188e1..dd992a9 100644 --- a/src/runtimes/acp/acp-permission-events.ts +++ b/src/runtimes/acp/acp-permission-events.ts @@ -60,7 +60,7 @@ export function toPermissionRequest(input: { rawInput: stringifyForDisplay(toolCall?.["rawInput"]), requestId: input.requestId, title, - toolCallId: nativeToolCallId, + toolCallId, toolKind: readNonEmptyString(toolCall, "kind"), }, targetItemId: toolCallId, diff --git a/src/runtimes/acp/acp-terminal-manager.ts b/src/runtimes/acp/acp-terminal-manager.ts index 6aa6c2e..a2fc6de 100644 --- a/src/runtimes/acp/acp-terminal-manager.ts +++ b/src/runtimes/acp/acp-terminal-manager.ts @@ -4,8 +4,9 @@ import { once } from "node:events"; import type { DriverEventInput } from "../../protocol/events"; import { createDriverId } from "../../protocol/id"; -import { settlePromiseWithTimeout } from "../../utils/async"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; +import { DriverEventRejectedError } from "../../core/driver-runtime-io"; +import { settlePromiseWithTimeout } from "../../utils/async"; import { bindSpawnedProcess, createProcessTreeEnvironment, @@ -30,20 +31,26 @@ import { AcpPathScope } from "./acp-path-scope"; interface AcpTerminalState { readonly closed: Promise; closedStatus: AcpTerminalExitStatus | null; - committed: boolean; + createEvent: DriverEventInput | null; + createEventTask: Promise | null; cleanupTakenOver: boolean; completionTask: Promise | null; readonly exited: Promise; + exitEvent: DriverEventInput | null; exitEventTask: Promise | null; exitStatus: AcpTerminalExitStatus | null; readonly id: string; + killEvent: DriverEventInput | null; + killTask: Promise | null; readonly marker: string; + hostMayOwn: boolean; orphaned: boolean; output: string; readonly outputByteLimit: number; readonly process: ChildProcessWithoutNullStreams; readonly rejectExit: (reason?: unknown) => void; readonly reservation: AcpTerminalReservation; + releaseEvent: DriverEventInput | null; releaseTask: Promise | null; readonly resolveExit: (status: AcpTerminalExitStatus) => void; supervisionFailure: Error | null; @@ -77,6 +84,7 @@ const DEFAULT_MAX_TERMINALS = 32; const DEFAULT_TERMINAL_OUTPUT_BYTE_LIMIT = 1024 * 1024; const TERMINAL_EXIT_TIMEOUT_MS = 2_000; const TERMINAL_FORCE_KILL_TIMEOUT_MS = 1_000; +const TERMINAL_KILL_WAIT_TIMEOUT_MS = TERMINAL_EXIT_TIMEOUT_MS + TERMINAL_FORCE_KILL_TIMEOUT_MS * 2; class AcpTerminalCleanupError extends Error { override readonly name = "AcpTerminalCleanupError"; @@ -211,20 +219,26 @@ export class AcpTerminalManager { const terminal: AcpTerminalState = { closed, closedStatus: null, - committed: false, + createEvent: null, + createEventTask: null, cleanupTakenOver: false, completionTask: null, exited, + exitEvent: null, exitEventTask: null, exitStatus: null, id: terminalId, + killEvent: null, + killTask: null, marker: processTree.marker, + hostMayOwn: false, orphaned: false, output: "", outputByteLimit, process: child, rejectExit, reservation, + releaseEvent: null, releaseTask: null, resolveExit, supervisionFailure: null, @@ -315,23 +329,23 @@ export class AcpTerminalManager { throw new Error(`ACP terminal ${terminalId} lost ownership during creation.`); } - await this.#push(context, "driver.acp.terminal.created", [ - { - kind: "terminal.created", - payload: { - command, - cwd: cwdPath, - outputByteLimit, - terminalId, - }, + terminal.createEvent = { + kind: "terminal.created", + payload: { + command, + cwd: cwdPath, + outputByteLimit, + terminalId, }, - ]); + sourceEventId: `acp.terminal.created:${terminalId}`, + }; + await this.#publishCreated(context, terminal); if (this.#terminals.get(terminalId) !== terminal) { throw new Error(`ACP terminal ${terminalId} lost ownership during creation.`); } - terminal.committed = true; + terminal.hostMayOwn = true; await this.#publishExit(context, terminal); signal?.throwIfAborted(); @@ -339,7 +353,18 @@ export class AcpTerminalManager { throw new Error("ACP terminal manager is stopping."); } } catch (error) { - if (terminal.committed) { + if ( + !terminal.hostMayOwn && + terminal.createEvent !== null && + !( + error instanceof DriverEventRejectedError && + error.sourceEventId === terminal.createEvent.sourceEventId + ) + ) { + terminal.hostMayOwn = true; + } + + if (terminal.hostMayOwn) { try { await this.#releaseTerminal(context, terminal); } catch (cleanupError) { @@ -382,24 +407,47 @@ export class AcpTerminalManager { ): Promise> { signal?.throwIfAborted(); const terminal = this.#requireTerminal(params); + await raceWithAbort(this.#killTerminal(context, terminal), signal); + + return {}; + } + + #killTerminal(context: AgentDriverContext, terminal: AcpTerminalState): Promise { + if (terminal.killTask !== null) { + return terminal.killTask; + } + + terminal.killEvent ??= { + kind: "terminal.killed", + payload: { + terminalId: terminal.id, + }, + sourceEventId: `acp.terminal.killed:${terminal.id}`, + }; + const operation = this.#runKill(context, terminal); + let task: Promise; + task = operation.catch((error: unknown) => { + if (terminal.killTask === task) { + terminal.killTask = null; + } + throw error; + }); + terminal.killTask = task; + return task; + } + async #runKill(context: AgentDriverContext, terminal: AcpTerminalState): Promise { if (terminal.exitStatus === null) { signalAcpTerminalProcess(terminal, "SIGKILL"); - await terminal.exited; + const exited = await this.#waitForExit(terminal, TERMINAL_KILL_WAIT_TIMEOUT_MS); + if (!exited) { + throw new Error( + `ACP terminal ${terminal.id} cleanup did not finish within ${TERMINAL_KILL_WAIT_TIMEOUT_MS}ms after force kill.`, + ); + } } - await raceWithAbort( - this.#push(context, "driver.acp.terminal.killed", [ - { - kind: "terminal.killed", - payload: { - terminalId: terminal.id, - }, - }, - ]), - signal, - ); - - return {}; + await this.#publishExit(context, terminal); + await this.#push(context, "driver.acp.terminal.killed", [terminal.killEvent!]); } output( @@ -457,6 +505,11 @@ export class AcpTerminalManager { async #stop(context: AgentDriverContext, turn?: number): Promise { const ownsTurn = (ownedTurn: number): boolean => turn === undefined || ownedTurn === turn; + const admittedKills = new Map( + [...this.#terminals.values()].flatMap((terminal) => + ownsTurn(terminal.reservation.turn) ? [[terminal, terminal.killTask] as const] : [], + ), + ); const creations = await Promise.allSettled( [...this.#createTasks].flatMap(([creation, ownedTurn]) => ownsTurn(ownedTurn) ? [creation] : [], @@ -464,7 +517,15 @@ export class AcpTerminalManager { ); const releases = await Promise.allSettled( [...this.#terminals.values()].flatMap((terminal) => - ownsTurn(terminal.reservation.turn) ? [this.#releaseTerminal(context, terminal)] : [], + ownsTurn(terminal.reservation.turn) + ? [ + this.#releaseTerminal( + context, + terminal, + admittedKills.get(terminal) ?? terminal.killTask, + ), + ] + : [], ), ); const failures = [ @@ -481,16 +542,26 @@ export class AcpTerminalManager { } } - #releaseTerminal(context: AgentDriverContext, terminal: AcpTerminalState): Promise { - return (terminal.releaseTask ??= this.#runRelease(context, terminal).catch((error: unknown) => { - if (this.#terminals.get(terminal.id) === terminal) { - terminal.releaseTask = null; - } - throw error; - })); + #releaseTerminal( + context: AgentDriverContext, + terminal: AcpTerminalState, + admittedKill: Promise | null = terminal.killTask, + ): Promise { + return (terminal.releaseTask ??= this.#runRelease(context, terminal, admittedKill).catch( + (error: unknown) => { + if (this.#terminals.get(terminal.id) === terminal) { + terminal.releaseTask = null; + } + throw error; + }, + )); } - async #runRelease(context: AgentDriverContext, terminal: AcpTerminalState): Promise { + async #runRelease( + context: AgentDriverContext, + terminal: AcpTerminalState, + admittedKill: Promise | null, + ): Promise { const wasRunning = terminal.closedStatus === null; if (wasRunning) { @@ -542,18 +613,27 @@ export class AcpTerminalManager { } terminal.exitStatus = terminal.closedStatus; } + if (terminal.hostMayOwn) { + await this.#publishCreated(context, terminal); + } await this.#publishExit(context, terminal); terminal.resolveExit(terminal.exitStatus); - if (terminal.committed) { - await this.#push(context, "driver.acp.terminal.released", [ - { - kind: "terminal.released", - payload: { - terminalId: terminal.id, - }, + if (admittedKill !== null) { + await admittedKill; + } else if (terminal.killEvent !== null) { + await this.#killTerminal(context, terminal); + } + + if (terminal.hostMayOwn) { + terminal.releaseEvent ??= { + kind: "terminal.released", + payload: { + terminalId: terminal.id, }, - ]); + sourceEventId: `acp.terminal.released:${terminal.id}`, + }; + await this.#push(context, "driver.acp.terminal.released", [terminal.releaseEvent]); } if (this.#terminals.get(terminal.id) === terminal) { @@ -622,8 +702,11 @@ export class AcpTerminalManager { } terminal.exitStatus = status; - await this.#publishExit(context, terminal); + const publication = this.#publishExit(context, terminal); terminal.resolveExit(status); + void publication.catch((error: unknown) => { + this.#warnPushFailure(context, "driver.acp.terminal.exited", error); + }); } #failSupervision(context: AgentDriverContext, terminal: AcpTerminalState, error: Error): void { @@ -647,7 +730,7 @@ export class AcpTerminalManager { terminal.output = appended.output; terminal.truncated ||= appended.truncated; - if (!terminal.committed) { + if (!terminal.hostMayOwn) { return; } @@ -716,28 +799,65 @@ export class AcpTerminalManager { events: DriverEventInput[], ): Promise { return this.#push(context, reason, events).catch((error: unknown) => { - context.logger.warn("driver.acp.terminal.event_push.failed", { - message: error instanceof Error ? error.message : "terminal event push failed", - reason, - }); + this.#warnPushFailure(context, reason, error); + }); + } + + #warnPushFailure(context: AgentDriverContext, reason: string, error: unknown): void { + context.logger.warn("driver.acp.terminal.event_push.failed", { + message: error instanceof Error ? error.message : "terminal event push failed", + reason, }); } #publishExit(context: AgentDriverContext, terminal: AcpTerminalState): Promise { - if (!terminal.committed || terminal.exitStatus === null) { + if (!terminal.hostMayOwn || terminal.exitStatus === null) { return Promise.resolve(); } - return (terminal.exitEventTask ??= this.#pushBestEffort(context, "driver.acp.terminal.exited", [ - { - kind: "terminal.exited", - payload: { - exitCode: terminal.exitStatus.exitCode, - signal: terminal.exitStatus.signal, - terminalId: terminal.id, - }, + terminal.exitEvent ??= { + kind: "terminal.exited", + payload: { + exitCode: terminal.exitStatus.exitCode, + signal: terminal.exitStatus.signal, + terminalId: terminal.id, }, - ])); + sourceEventId: `acp.terminal.exited:${terminal.id}`, + }; + if (terminal.exitEventTask !== null) { + return terminal.exitEventTask; + } + + const operation = this.#push(context, "driver.acp.terminal.exited", [terminal.exitEvent]); + let task: Promise; + task = operation.catch((error: unknown) => { + if (terminal.exitEventTask === task) { + terminal.exitEventTask = null; + } + throw error; + }); + terminal.exitEventTask = task; + return task; + } + + #publishCreated(context: AgentDriverContext, terminal: AcpTerminalState): Promise { + if (terminal.createEvent === null) { + return Promise.resolve(); + } + if (terminal.createEventTask !== null) { + return terminal.createEventTask; + } + + const operation = this.#push(context, "driver.acp.terminal.created", [terminal.createEvent]); + let task: Promise; + task = operation.catch((error: unknown) => { + if (terminal.createEventTask === task) { + terminal.createEventTask = null; + } + throw error; + }); + terminal.createEventTask = task; + return task; } #requireTerminal(params: unknown): AcpTerminalState { @@ -749,7 +869,11 @@ export class AcpTerminalManager { const terminal = this.#terminals.get(terminalId); - if (!terminal || (!terminal.committed && !terminal.orphaned)) { + if ( + !terminal || + terminal.releaseTask !== null || + (!terminal.hostMayOwn && !terminal.orphaned) + ) { throw new Error(`ACP terminal does not exist: ${terminalId}.`); } diff --git a/src/runtimes/acp/acp-turn-controller.ts b/src/runtimes/acp/acp-turn-controller.ts index 28d0e41..bb6e278 100644 --- a/src/runtimes/acp/acp-turn-controller.ts +++ b/src/runtimes/acp/acp-turn-controller.ts @@ -69,6 +69,7 @@ async function drainTurnWork( signal?: AbortSignal, ): Promise { const results = await Promise.allSettled([ + clientRequests.drainTurnFileWrites(signal), clientRequests.drainPermissions(signal), signal === undefined ? clientRequests.drainUpdates() @@ -192,6 +193,7 @@ export class AcpTurnController { throw new Error("ACP driver backend already has an active turn."); } + clientRequests.openFileWriteIngress(); const messageId = createDriverId() as MessageId; const active = createActiveTurn(runId, clientRequests.beginTurnTerminals()); this.#active = active; @@ -264,6 +266,7 @@ export class AcpTurnController { ); if (active.cancelRequested) { + clientRequests.closeFileWriteIngress(); clientRequests.closePermissionIngress(); clientRequests.closeTurnTranscriptIngress(); await drain(); @@ -289,6 +292,7 @@ export class AcpTurnController { prompt: [{ text: input.text, type: "text" }], sessionId, }); + clientRequests.closeFileWriteIngress(); if (active.fatal !== null) { throw active.fatal.error; } @@ -331,6 +335,7 @@ export class AcpTurnController { } } catch (error) { active.providerPromptSettled.resolve(active.providerPromptAdmitted && active.cancelRequested); + clientRequests.closeFileWriteIngress(); clientRequests.closePermissionIngress(); clientRequests.closeTurnTranscriptIngress(); if (preserveEventState) { @@ -438,6 +443,7 @@ export class AcpTurnController { ); throw error; } finally { + clientRequests.closeFileWriteIngress(); clientRequests.closePermissionIngress(); clientRequests.closeTurnTranscriptIngress(); if (active.drainDeadline !== null) { diff --git a/src/runtimes/claude/agent-sdk-event-writer.ts b/src/runtimes/claude/agent-sdk-event-writer.ts index 80c4eac..4497c28 100644 --- a/src/runtimes/claude/agent-sdk-event-writer.ts +++ b/src/runtimes/claude/agent-sdk-event-writer.ts @@ -350,11 +350,7 @@ export class ClaudeAgentSdkEventWriter { runId, kind: "run.completed", payload: { - ...(finalMessage === null - ? {} - : { - finalMessageId: finalMessage.id, - }), + ...(finalMessage === null ? {} : { finalMessageId: finalMessage.id }), stopReason: "end_turn", ...(structuredOutput === undefined ? {} : { structuredOutput }), }, @@ -533,7 +529,7 @@ export class ClaudeAgentSdkEventWriter { delivery: "best_effort", kind: "tool.call.updated", payload: { - rawInput: delta, + rawInputDelta: delta, status: "running", toolCallId, }, diff --git a/src/runtimes/claude/agent-sdk-message-translator.ts b/src/runtimes/claude/agent-sdk-message-translator.ts index 882d0b5..87a14ad 100644 --- a/src/runtimes/claude/agent-sdk-message-translator.ts +++ b/src/runtimes/claude/agent-sdk-message-translator.ts @@ -448,13 +448,8 @@ export class ClaudeAgentSdkMessageTranslator { } const { input } = block; - if (input) { - await this.#events.pushToolArguments({ - context, - delta: stringifyForDisplay(input), - reason: "driver.claude.tool.args", - toolCallId, - }); + if (input && (!isRecord(input) || Object.keys(input).length > 0)) { + await this.#events.pushToolSnapshot(context, toolCallId, stringifyForDisplay(input)); } } @@ -689,7 +684,7 @@ export class ClaudeAgentSdkMessageTranslator { ): Promise { const { diagnostic, snapshot } = projectClaudeBackgroundTasksSnapshot(message); await this.#options.push(context, "driver.claude.tasks.replaced", [ - ...(snapshot === null ? [] : [snapshot]), + snapshot, ...(diagnostic === undefined ? [] : [diagnostic]), ]); } @@ -896,7 +891,7 @@ export class ClaudeAgentSdkMessageTranslator { // resume can omit every assistant frame, so the result is materialized // above only when no competing text candidate exists. const finalMessage = - structuredOutput !== undefined || resultText === null + structuredOutput !== undefined || resultText === null || resultText.trim().length === 0 ? null : this.#state.resolveFinalAssistantSnapshot(runId, resultText); diff --git a/src/runtimes/claude/agent-sdk-task-events.ts b/src/runtimes/claude/agent-sdk-task-events.ts index 5bf9ac9..bd80f6d 100644 --- a/src/runtimes/claude/agent-sdk-task-events.ts +++ b/src/runtimes/claude/agent-sdk-task-events.ts @@ -15,7 +15,7 @@ const MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS = 256; export interface ClaudeBackgroundTasksProjection { readonly diagnostic?: DriverEventInput; - readonly snapshot: DriverEventInput | null; + readonly snapshot: DriverEventInput; } function taskSnapshotDiagnostic(code: string, taskCount: number): DriverEventInput { @@ -36,7 +36,10 @@ function taskSnapshotDiagnostic(code: string, taskCount: number): DriverEventInp } function rejectedTaskSnapshot(code: string, taskCount: number): ClaudeBackgroundTasksProjection { - return { diagnostic: taskSnapshotDiagnostic(code, taskCount), snapshot: null }; + return { + diagnostic: taskSnapshotDiagnostic(code, taskCount), + snapshot: claudeBackgroundTasksClosedEvent(), + }; } function publicTaskId(nativeTaskId: string): string | null { @@ -94,7 +97,7 @@ export function projectClaudeBackgroundTasksSnapshot( ...(title === undefined ? {} : { title }), }); if (tasks.size > MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS) { - return rejectedTaskSnapshot("claude.visible_background_tasks_too_many", message.tasks.length); + return rejectedTaskSnapshot("claude.visible_background_tasks_too_many", tasks.size); } } diff --git a/src/runtimes/driver-event-publisher.ts b/src/runtimes/driver-event-publisher.ts index 47e65ac..f942c5d 100644 --- a/src/runtimes/driver-event-publisher.ts +++ b/src/runtimes/driver-event-publisher.ts @@ -67,6 +67,17 @@ interface QueuedPush { readonly terminalKey: string | null; } +interface PendingAdoption { + readonly events: DriverEventInput[]; + readonly sourceEventIds: string[]; +} + +interface EventSettlement { + readonly promise: Promise; + readonly reject: (reason?: unknown) => void; + readonly resolve: () => void; +} + class QueuedPushDeliveryError extends Error { readonly deliveryCause: unknown; readonly retainedSourceEventIds: readonly string[]; @@ -88,6 +99,7 @@ export class DriverEventPublisher { readonly #runtime: DriverRuntime; #acceptedInFlightSourceEventIds = new Set(); #drainTask: Promise | null = null; + #eventSettlements = new Map(); #inFlightEvents: readonly QueuedDriverEvent[] | null = null; #lastAcceptedSeq = 0; #pendingEvents: QueuedDriverEvent[] = []; @@ -183,7 +195,6 @@ export class DriverEventPublisher { if ( terminalSettlement === undefined && - activeRunId !== null && [...explicitRunIds].some((runId) => runId !== activeRunId) ) { if (events.some(isLosslessDriverEvent)) { @@ -239,28 +250,113 @@ export class DriverEventPublisher { return Promise.resolve(); } - const entry = this.#admit(context, reason, events, activeRunId, terminalSettlement?.signal); - - if (entry === null) { + const candidates = [ + ...(this.#inFlightEvents ?? this.#pendingEvents), + ...this.#queue.flatMap((queued) => queued.events), + ]; + const adoption = + terminalSettlement === undefined + ? this.#adoptPending(events, activeRunId, candidates) + : { events: [...events], sourceEventIds: [] }; + const entry = + adoption.events.length === 0 + ? null + : this.#admit(context, reason, adoption.events, activeRunId, terminalSettlement?.signal); + + if (entry === null && adoption.sourceEventIds.length === 0) { this.#requestPendingDrain(context, reason); return Promise.resolve(); } - if (terminalSettlement === undefined) { - const unresolvedSourceEventIds = new Set( - [ - ...(this.#inFlightEvents ?? this.#pendingEvents), - ...this.#queue.flatMap((queued) => queued.events), - ].map(({ event }) => event.sourceEventId), - ); + if (entry !== null && terminalSettlement === undefined) { + const unresolvedSourceEventIds = new Set(candidates.map(({ event }) => event.sourceEventId)); if (entry.events.some(({ event }) => unresolvedSourceEventIds.has(event.sourceEventId))) { throw new Error("Driver source event ID conflicts with a pending event."); } } - this.#enqueue(entry); - return entry.losslessCount === 0 && !entry.terminalBatch ? Promise.resolve() : entry.promise; + const deliveries: Promise[] = []; + + if (entry !== null) { + this.#enqueue(entry); + if (entry.losslessCount > 0 || entry.terminalBatch) { + deliveries.push(entry.promise); + } + } + + if (adoption.sourceEventIds.length > 0) { + const acknowledged = Promise.all( + adoption.sourceEventIds.map((sourceEventId) => { + const settlement = this.#eventSettlements.get(sourceEventId); + if (settlement === undefined) { + throw new Error("Driver pending event settlement is missing."); + } + return settlement.promise; + }), + ).then(() => undefined); + const retry = this.#createPendingRetry( + context, + reason, + adoption.sourceEventIds, + terminalSettlement?.signal, + ); + this.#enqueue(retry); + deliveries.push(Promise.race([acknowledged, retry.promise.then(() => acknowledged)])); + } + + return deliveries.length === 0 + ? Promise.resolve() + : Promise.all(deliveries).then(() => undefined); + } + + #adoptPending( + events: readonly DriverEventInput[], + activeRunId: RunId | null, + candidates: readonly QueuedDriverEvent[], + ): PendingAdoption { + const adopted = new Set(); + const fresh: DriverEventInput[] = []; + const sourceEventIds: string[] = []; + + for (const event of events) { + if (!isLosslessDriverEvent(event)) { + fresh.push(event); + continue; + } + + const explicitSourceEventId = + typeof event.sourceEventId === "string" && event.sourceEventId.length > 0 + ? event.sourceEventId + : undefined; + if (explicitSourceEventId === undefined) { + fresh.push(event); + continue; + } + const candidateIndex = candidates.findIndex( + ({ event: candidate }) => candidate.sourceEventId === explicitSourceEventId, + ); + + if (candidateIndex < 0) { + fresh.push(event); + continue; + } + + const retryKey = losslessDriverEventRetryKey(event, activeRunId)!; + if (adopted.has(candidateIndex)) { + throw new Error("Driver event push requires unique source event IDs."); + } + + const candidate = candidates[candidateIndex]!.event; + if (losslessDriverEventRetryKey(candidate, activeRunId) !== retryKey) { + throw new Error("Driver source event ID conflicts with a pending event."); + } + + adopted.add(candidateIndex); + sourceEventIds.push(candidate.sourceEventId!); + } + + return { events: fresh, sourceEventIds }; } async #retryPending( @@ -512,6 +608,21 @@ export class DriverEventPublisher { } #enqueue(entry: QueuedPush): void { + for (const { event } of entry.events) { + const sourceEventId = event.sourceEventId; + if ( + !isLosslessDriverEvent(event) || + sourceEventId === undefined || + this.#eventSettlements.has(sourceEventId) + ) { + continue; + } + + const settlement = Promise.withResolvers(); + void settlement.promise.catch(() => {}); + this.#eventSettlements.set(sourceEventId, settlement); + } + this.#queue.push(entry); this.#queuedEventBytes += entry.eventBytes; this.#queuedEventCount += entry.eventCount; @@ -755,6 +866,7 @@ export class DriverEventPublisher { remainingLossless.splice(retainedIndex, 1); this.#rejectedInFlight.set(error.sourceEventId, error); + this.#eventSettlements.get(error.sourceEventId)?.reject(error); const settlement = this.#terminalSettlement; @@ -767,17 +879,17 @@ export class DriverEventPublisher { settlement.rejection ??= error; } - if (rejected!.owner === null) { - for (const entry of entries) { - if ( - entry.awaitPending && - entry.retrySourceEventIds.includes(error.sourceEventId) && - !ownerErrors.has(entry.id) - ) { - ownerErrors.set(entry.id, error); - } + for (const entry of entries) { + if ( + entry.awaitPending && + entry.retrySourceEventIds.includes(error.sourceEventId) && + !ownerErrors.has(entry.id) + ) { + ownerErrors.set(entry.id, error); } - } else if (!ownerErrors.has(rejected!.owner)) { + } + + if (rejected!.owner !== null && !ownerErrors.has(rejected!.owner)) { ownerErrors.set(rejected!.owner, error); } } @@ -808,6 +920,16 @@ export class DriverEventPublisher { } this.#setPending(remainingLossless, terminalKey); + const pendingSourceEventIds = new Set( + this.#pendingEvents.map(({ event }) => event.sourceEventId), + ); + + for (const { event } of queuedEvents) { + const sourceEventId = event.sourceEventId; + if (sourceEventId !== undefined && !pendingSourceEventIds.has(sourceEventId)) { + this.#eventSettlements.delete(sourceEventId); + } + } context.logger.debug("driver.runtime.events.sent", { acceptedEventCount, @@ -824,9 +946,7 @@ export class DriverEventPublisher { .filter( ({ event, owner }) => owner === entry.id || - (entry.awaitPending && - owner === null && - entry.retrySourceEventIds.includes(event.sourceEventId!)), + (entry.awaitPending && entry.retrySourceEventIds.includes(event.sourceEventId!)), ) .map(({ event }) => event.sourceEventId!); const error = @@ -885,6 +1005,7 @@ export class DriverEventPublisher { for (const event of events) { if (event.sourceEventId !== undefined) { this.#acceptedInFlightSourceEventIds.add(event.sourceEventId); + this.#eventSettlements.get(event.sourceEventId)?.resolve(); } } diff --git a/src/runtimes/mcp/remote-http-mcp-executor.ts b/src/runtimes/mcp/remote-http-mcp-executor.ts index c07acac..0aa1cc6 100644 --- a/src/runtimes/mcp/remote-http-mcp-executor.ts +++ b/src/runtimes/mcp/remote-http-mcp-executor.ts @@ -1,6 +1,7 @@ import { Client, InsufficientScopeError, + LATEST_PROTOCOL_VERSION, ProtocolError, ProtocolErrorCode, SdkError, @@ -12,7 +13,9 @@ import { import type { AuthProvider, CallToolResult } from "@modelcontextprotocol/client"; import { AGENT_DRIVER_VERSION } from "../../core/version"; +import { AGENT_DRIVER_MCP_EXECUTE_TIMEOUT_MS } from "../../host-ports"; import type { AgentDriverMcpExecution } from "../../host-ports"; +import type { Logger } from "../../observability"; import type { DriverStartInput } from "../../protocol/start"; import type { McpExecuteCommand, McpExternalToolExecutionResult } from "../../runtime-command"; import { settlePromiseWithTimeout } from "../../utils/async"; @@ -20,10 +23,68 @@ import { settlePromiseWithTimeout } from "../../utils/async"; type SessionMcpServer = DriverStartInput["execution"]["session"]["mcpServers"][number]; type ActiveMcpServer = Extract; -const MCP_REQUEST_TIMEOUT_MS = 60_000; +const MCP_CONNECT_TIMEOUT_MS = 60_000; const MCP_CLEANUP_TIMEOUT_MS = 2_000; +const MCP_RESPONSE_MAX_BYTES = 8 * 1_024 * 1_024; const MOSOO_TOOL_CALL_ID_HEADER = "X-Mosoo-Tool-Call-Id"; +class McpResponseTooLargeError extends RangeError { + constructor() { + super(`MCP response exceeds ${String(MCP_RESPONSE_MAX_BYTES)} bytes.`); + this.name = "McpResponseTooLargeError"; + } +} + +async function boundedMcpFetch( + input: string | URL | Request, + init?: RequestInit, +): Promise { + const response = await fetch(input, init); + const contentLength = response.headers.get("content-length"); + + if (contentLength !== null && Number(contentLength) > MCP_RESPONSE_MAX_BYTES) { + void response.body?.cancel().catch(() => {}); + const body = + response.body === null + ? null + : new ReadableStream({ + start(controller) { + controller.error(new McpResponseTooLargeError()); + }, + }); + + return new Response(body, { + headers: response.headers, + status: response.status, + statusText: response.statusText, + }); + } + if (response.body === null) { + return response; + } + + let bytes = 0; + const body = response.body.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + if (chunk.byteLength > MCP_RESPONSE_MAX_BYTES - bytes) { + controller.error(new McpResponseTooLargeError()); + return; + } + + bytes += chunk.byteLength; + controller.enqueue(chunk); + }, + }), + ); + + return new Response(body, { + headers: response.headers, + status: response.status, + statusText: response.statusText, + }); +} + function parseToolArguments(command: McpExecuteCommand): Record { try { const parsed: unknown = JSON.parse(command.argumentsJson); @@ -218,30 +279,137 @@ function mapMcpExecutionError( return new Error(`Failed to execute MCP tool ${command.toolName} on ${server.name}.`); } -async function closeMcpConnection( +function cleanupFailureMessage( + result: Awaited>, +): string { + if (result.status === "completed") { + return "Cleanup completed."; + } + + return result.error instanceof Error ? result.error.message : "Unknown cleanup failure."; +} + +function createMcpTransport( + proxyUrl: URL, + authProvider: AuthProvider, + command: McpExecuteCommand, + session?: { readonly protocolVersion?: string; readonly sessionId: string }, +): StreamableHTTPClientTransport { + return new StreamableHTTPClientTransport(proxyUrl, { + authProvider, + fetch: boundedMcpFetch, + requestInit: { + headers: { [MOSOO_TOOL_CALL_ID_HEADER]: command.toolCallId }, + }, + ...session, + }); +} + +async function closeMcpClient( client: Client, - transport: StreamableHTTPClientTransport, + logger: Logger, + command: McpExecuteCommand, ): Promise { - await settlePromiseWithTimeout( - Promise.resolve().then(() => transport.terminateSession()), + const close = await settlePromiseWithTimeout( + Promise.resolve().then(() => client.close()), { - label: "MCP session termination", + label: "MCP client close", timeoutMs: MCP_CLEANUP_TIMEOUT_MS, }, ); - await settlePromiseWithTimeout( - Promise.resolve().then(() => client.close()), + if (close.status !== "completed") { + logger.warn("driver.mcp.client-close.failed", { + commandId: command.commandId, + message: cleanupFailureMessage(close), + serverId: command.serverId, + status: close.status, + }); + } +} + +async function terminateFailedConnectionSession( + proxyUrl: URL, + authProvider: AuthProvider, + session: { readonly protocolVersion?: string; readonly sessionId: string }, + logger: Logger, + command: McpExecuteCommand, +): Promise { + const cleanupTransport = createMcpTransport(proxyUrl, authProvider, command, session); + const termination = await settlePromiseWithTimeout( + Promise.resolve().then(async () => { + await cleanupTransport.start(); + await cleanupTransport.terminateSession(); + }), { - label: "MCP client close", + label: "Failed MCP connection session termination", + timeoutMs: MCP_CLEANUP_TIMEOUT_MS, + }, + ); + const close = await settlePromiseWithTimeout( + Promise.resolve().then(() => cleanupTransport.close()), + { + label: "Failed MCP connection cleanup transport close", timeoutMs: MCP_CLEANUP_TIMEOUT_MS, }, ); + + if (termination.status !== "completed") { + logger.warn("driver.mcp.session-termination.failed", { + commandId: command.commandId, + message: cleanupFailureMessage(termination), + serverId: command.serverId, + status: termination.status, + }); + } + if (close.status !== "completed") { + logger.warn("driver.mcp.cleanup-transport-close.failed", { + commandId: command.commandId, + message: cleanupFailureMessage(close), + serverId: command.serverId, + status: close.status, + }); + } +} + +async function closeMcpConnection( + client: Client, + transport: StreamableHTTPClientTransport, + logger: Logger, + command: McpExecuteCommand, +): Promise { + let termination = await settlePromiseWithTimeout( + Promise.resolve().then(() => transport.terminateSession()), + { + label: "MCP session termination", + timeoutMs: MCP_CLEANUP_TIMEOUT_MS, + }, + ); + + if (termination.status === "failed") { + termination = await settlePromiseWithTimeout( + Promise.resolve().then(() => transport.terminateSession()), + { + label: "MCP session termination retry", + timeoutMs: MCP_CLEANUP_TIMEOUT_MS, + }, + ); + } + if (termination.status !== "completed") { + logger.warn("driver.mcp.session-termination.failed", { + commandId: command.commandId, + message: cleanupFailureMessage(termination), + serverId: command.serverId, + status: termination.status, + }); + } + await closeMcpClient(client, logger, command); } export async function prepareRemoteHttpMcpCommand( payload: DriverStartInput, command: McpExecuteCommand, signal: AbortSignal, + logger: Logger, ): Promise { signal.throwIfAborted(); const server = resolveActiveMcpServer(payload, command); @@ -259,34 +427,46 @@ export async function prepareRemoteHttpMcpCommand( versionNegotiation: { mode: "auto" }, }, ); - const transport = new StreamableHTTPClientTransport(proxyUrl, { - authProvider, - requestInit: { - headers: { [MOSOO_TOOL_CALL_ID_HEADER]: command.toolCallId }, - }, - }); - const connectSignal = AbortSignal.any([signal, AbortSignal.timeout(MCP_REQUEST_TIMEOUT_MS)]); + const transport = createMcpTransport(proxyUrl, authProvider, command); + const connectSignal = AbortSignal.any([signal, AbortSignal.timeout(MCP_CONNECT_TIMEOUT_MS)]); try { await client.connect(transport, { signal: connectSignal, - timeout: MCP_REQUEST_TIMEOUT_MS, + timeout: MCP_CONNECT_TIMEOUT_MS, }); } catch (error) { - await closeMcpConnection(client, transport); + const failedSession = + transport.sessionId === undefined + ? undefined + : { + protocolVersion: transport.protocolVersion ?? LATEST_PROTOCOL_VERSION, + sessionId: transport.sessionId, + }; + + await closeMcpClient(client, logger, command); + if (failedSession !== undefined) { + await terminateFailedConnectionSession( + proxyUrl, + authProvider, + failedSession, + logger, + command, + ); + } throw mapMcpExecutionError(command, server, error); } - let disposed = false; + let disposeTask: Promise | null = null; let executed = false; return { async execute(effect): Promise { - if (disposed || executed) { + if (disposeTask !== null || executed) { throw new Error(`Prepared MCP command ${command.commandId} can only be executed once.`); } executed = true; - const requestSignal = AbortSignal.any([signal, AbortSignal.timeout(MCP_REQUEST_TIMEOUT_MS)]); + const requestSignal = AbortSignal.timeout(AGENT_DRIVER_MCP_EXECUTE_TIMEOUT_MS); try { const result = await client.callTool( @@ -299,7 +479,7 @@ export async function prepareRemoteHttpMcpCommand( }, { signal: requestSignal, - timeout: MCP_REQUEST_TIMEOUT_MS, + timeout: AGENT_DRIVER_MCP_EXECUTE_TIMEOUT_MS, }, ); @@ -309,11 +489,7 @@ export async function prepareRemoteHttpMcpCommand( } }, async [Symbol.asyncDispose](): Promise { - if (disposed) { - return; - } - disposed = true; - await closeMcpConnection(client, transport); + return (disposeTask ??= closeMcpConnection(client, transport, logger, command)); }, }; } diff --git a/src/runtimes/openai/app-server-agent-task-events.ts b/src/runtimes/openai/app-server-agent-task-events.ts index 41017e3..4386778 100644 --- a/src/runtimes/openai/app-server-agent-task-events.ts +++ b/src/runtimes/openai/app-server-agent-task-events.ts @@ -30,7 +30,7 @@ interface OpenAiAgentTaskUpdate { readonly events: DriverEventInput[]; } -function publicTaskId(nativeTaskId: string): string { +export function toOpenAiAgentTaskId(nativeTaskId: string): string { return Buffer.byteLength(nativeTaskId, "utf8") <= MAX_OPENAI_AGENT_TASK_ID_BYTES ? nativeTaskId : `openai-task:${createHash("sha256").update(nativeTaskId).digest("hex")}`; @@ -84,7 +84,7 @@ export class OpenAiAgentTaskState { prepare(activity: OpenAiSubAgentActivity): OpenAiAgentTaskUpdate { const closedTaskIds = new Set(this.#closedTaskIds); const tasks = new Map(this.#tasks); - const taskId = publicTaskId(activity.agentId); + const taskId = toOpenAiAgentTaskId(activity.agentId); if (activity.kind === "interacted") { closedTaskIds.delete(taskId); diff --git a/src/runtimes/openai/app-server-driver-backend.ts b/src/runtimes/openai/app-server-driver-backend.ts index ef16580..43f29a8 100644 --- a/src/runtimes/openai/app-server-driver-backend.ts +++ b/src/runtimes/openai/app-server-driver-backend.ts @@ -374,6 +374,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { const threadStartParams = { ...baseThreadParams, ...(developerInstructions === null ? {} : { developerInstructions }), + historyMode: "paginated", sessionStartSource: "startup", } satisfies ThreadStartParams; @@ -393,6 +394,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { { ...baseThreadParams, ...(developerInstructions === null ? {} : { developerInstructions }), + excludeTurns: true, threadId: resumeThreadId, } satisfies ThreadResumeParams, signal, diff --git a/src/runtimes/openai/app-server-event-bridge.ts b/src/runtimes/openai/app-server-event-bridge.ts index b151811..e70c08d 100644 --- a/src/runtimes/openai/app-server-event-bridge.ts +++ b/src/runtimes/openai/app-server-event-bridge.ts @@ -1,6 +1,6 @@ import { DriverTurnCancelledError } from "../../core/driver-runtime-state"; import type { DriverEventInput } from "../../protocol/events"; -import type { RunId } from "../../protocol/id"; +import { createDriverId, type RunId } from "../../protocol/id"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; import { toOpenAiProtocolError } from "./app-server-event-mapping"; import { @@ -104,6 +104,7 @@ function toOpenAiWarningEvents( ...(chunks.length === 1 ? {} : { chunkCount: chunks.length, chunkIndex: index }), content, level: "warning", + messageId: createDriverId(), role: "agent", subtype: method === "guardianWarning" ? "guardian_warning" : "warning", }, @@ -412,6 +413,7 @@ export class OpenAiAppServerEventBridge { content: "This request requires additional safety checks; tool calls may take longer.", level: "warning", + messageId: createDriverId(), role: "agent", subtype: "strict_review_required", }, @@ -949,6 +951,7 @@ export class OpenAiAppServerEventBridge { extraCount, failedScan, level: "warning", + messageId: createDriverId(), role: "agent", samplePaths, subtype: "windows_world_writable_warning", @@ -993,6 +996,10 @@ export class OpenAiAppServerEventBridge { params, turnId, ); + const authoritativeFinalSnapshot = + authoritativeFinalMessage !== null && authoritativeFinalMessage.text.trim().length > 0 + ? authoritativeFinalMessage + : null; if (!this.#turns.beginSettlement(turnId)) { return; } @@ -1088,11 +1095,9 @@ export class OpenAiAppServerEventBridge { }), kind: "run.completed", payload: { - ...(authoritativeFinalMessage === null + ...(authoritativeFinalSnapshot === null ? {} - : { - finalMessageId: authoritativeFinalMessage.id, - }), + : { finalMessageId: authoritativeFinalSnapshot.id }), stopReason: "end_turn", }, }, diff --git a/src/runtimes/openai/app-server-event-mapping.ts b/src/runtimes/openai/app-server-event-mapping.ts index 224981f..00f7d83 100644 --- a/src/runtimes/openai/app-server-event-mapping.ts +++ b/src/runtimes/openai/app-server-event-mapping.ts @@ -6,10 +6,20 @@ const retryableCodexErrors = new Set([ "httpConnectionFailed", "responseStreamConnectionFailed", "internalServerError", + "rateLimitExceeded", "responseStreamDisconnected", ]); const MAX_OPENAI_ERROR_TEXT_BYTES = 16 * 1_024; +type OpenAiMisalignmentDetails = { + readonly misalignmentDetailedExplanation?: string; + readonly misalignmentDetailedExplanationUtf8Bytes?: number; + readonly misalignmentErrorType?: string; + readonly misalignmentErrorTypeUtf8Bytes?: number; + readonly misalignmentSteerMessage?: string; + readonly misalignmentSteerMessageUtf8Bytes?: number; +}; + function boundOpenAiErrorText( text: string, label: string, @@ -46,6 +56,50 @@ function toOpenAiErrorMessage( return `${message}\n${details}`; } +function toOpenAiMisalignmentDetails(error: JsonObject): OpenAiMisalignmentDetails | null { + const misalignment = isRecord(error["misalignment"]) ? error["misalignment"] : null; + if (misalignment === null) { + return null; + } + + const errorType = readString(misalignment, "errorType"); + const detailedExplanation = readString(misalignment, "detailedExplanation"); + const steer = isRecord(misalignment["steer"]) ? misalignment["steer"] : null; + const steerMessage = readString(steer, "message"); + const boundedErrorType = + errorType === null ? null : boundOpenAiErrorText(errorType, "OpenAI misalignment error type"); + const boundedExplanation = + detailedExplanation === null + ? null + : boundOpenAiErrorText(detailedExplanation, "OpenAI misalignment explanation"); + const boundedSteerMessage = + steerMessage === null + ? null + : boundOpenAiErrorText(steerMessage, "OpenAI misalignment steering message"); + const details: OpenAiMisalignmentDetails = { + ...(boundedErrorType?.utf8Bytes === null + ? { misalignmentErrorType: boundedErrorType.text } + : {}), + ...(boundedErrorType === null || boundedErrorType.utf8Bytes === null + ? {} + : { misalignmentErrorTypeUtf8Bytes: boundedErrorType.utf8Bytes }), + ...(boundedExplanation?.utf8Bytes === null + ? { misalignmentDetailedExplanation: boundedExplanation.text } + : {}), + ...(boundedExplanation === null || boundedExplanation.utf8Bytes === null + ? {} + : { misalignmentDetailedExplanationUtf8Bytes: boundedExplanation.utf8Bytes }), + ...(boundedSteerMessage?.utf8Bytes === null + ? { misalignmentSteerMessage: boundedSteerMessage.text } + : {}), + ...(boundedSteerMessage === null || boundedSteerMessage.utf8Bytes === null + ? {} + : { misalignmentSteerMessageUtf8Bytes: boundedSteerMessage.utf8Bytes }), + }; + + return Object.keys(details).length === 0 ? null : details; +} + function classifyOpenAiError(info: unknown): { codexErrorInfo?: string; httpStatusCode?: number; @@ -89,12 +143,14 @@ export function toOpenAiProtocolError(error: JsonObject | null): ProtocolError { ? null : boundOpenAiErrorText(rawAdditionalDetails, "OpenAI provider error details"); const { retryable, ...classification } = classifyOpenAiError(error["codexErrorInfo"]); + const misalignment = toOpenAiMisalignmentDetails(error); const details = { ...(additionalDetails === null ? {} : { additionalDetails: additionalDetails.text }), ...(additionalDetails?.utf8Bytes === null || additionalDetails === null ? {} : { additionalDetailsUtf8Bytes: additionalDetails.utf8Bytes }), ...classification, + ...misalignment, ...(message.utf8Bytes === null ? {} : { messageUtf8Bytes: message.utf8Bytes }), }; diff --git a/src/runtimes/openai/app-server-item-events.ts b/src/runtimes/openai/app-server-item-events.ts index b60cc5b..fd93c75 100644 --- a/src/runtimes/openai/app-server-item-events.ts +++ b/src/runtimes/openai/app-server-item-events.ts @@ -19,6 +19,7 @@ import type { JsonObject } from "./app-server-json"; import { OpenAiAgentTaskState, openAiAgentTasksClosedEvent, + toOpenAiAgentTaskId, type OpenAiSubAgentActivity, } from "./app-server-agent-task-events"; import { @@ -156,10 +157,11 @@ function toOpenAiItemLifecycleEvents( case "imageView": case "sleep": return []; - // The input path owns user messages, and hook/* notifications own hook lifecycle. - // Their ThreadItem copies are provider echoes and must not publish duplicates. + // User messages and standalone function outputs are input echoes, while hook/* + // notifications own hook lifecycle. Their ThreadItem copies must not publish duplicates. case "userMessage": case "hookPrompt": + case "functionCallOutput": return []; case "subAgentActivity": { // App-server emits a started/completed pair for one display activity. Publish it once. @@ -186,7 +188,7 @@ function toOpenAiItemLifecycleEvents( activityKind, agentId, agentPath, - taskId: agentId, + taskId: toOpenAiAgentTaskId(agentId), title: `Sub-agent ${activityKind}`, }, }, @@ -676,7 +678,7 @@ export class OpenAiAppServerItemEventBridge { kind: "tool.call.updated", payload: { messageId: parentMessageId, - rawOutput: delta, + rawOutputDelta: delta, status: "running", toolCallId: publicToolCallId, }, @@ -776,19 +778,32 @@ export class OpenAiAppServerItemEventBridge { }; if (this.#messages.needsSnapshot(snapshot)) { + const sourcePrefix = `openai.turn.final_message:${publicTurnId}:${publicItemId}`; + const snapshotEvents = toOpenAiMessageSnapshotEvents({ + item: finalAssistantItem, + itemId, + messageId, + phase, + sourcePrefix, + text: filteredText.text, + }); await this.#push( context, "driver.openai.turn.final_message", withOpenAiEventIds( - toOpenAiMessageSnapshotEvents({ - item: finalAssistantItem, - itemId, - messageId, - phase, - sourcePrefix: `openai.turn.final_message:${publicTurnId}:${publicItemId}`, - text: filteredText.text, - }), - `openai.turn.final_message:${publicTurnId}:${publicItemId}`, + [ + ...snapshotEvents, + ...(this.#messages.isEnded(messageId) + ? [ + { + kind: "message.completed" as const, + payload: { messageId, role: "agent" }, + sourceEventId: `${sourcePrefix}:completed`, + }, + ] + : []), + ], + sourcePrefix, ), ); this.#messages.setText(messageId, filteredText.text); diff --git a/src/runtimes/openai/generated-json-schema/README.md b/src/runtimes/openai/generated-json-schema/README.md index ff0d31d..d990c80 100644 --- a/src/runtimes/openai/generated-json-schema/README.md +++ b/src/runtimes/openai/generated-json-schema/README.md @@ -1,22 +1,7 @@ # OpenAI app-server JSON schemas -These eight runtime schemas are selected from `@openai/codex@0.150.1` output and must not be edited by hand. +These eight runtime schemas are selected from `@openai/codex@0.151.0` output and must not be edited by hand. ```sh -set -eu -vp exec codex --version -schema_tmp=$(mktemp -d) -trap 'rm -rf "$schema_tmp"' EXIT -schema_dir=src/runtimes/openai/generated-json-schema -vp exec codex app-server generate-json-schema --experimental --out "$schema_tmp" -fd --extension json . "$schema_dir" --exec rm -cp "$schema_tmp/ServerNotification.json" "$schema_dir/ServerNotification.json" -cp "$schema_tmp/ServerRequest.json" "$schema_dir/ServerRequest.json" -cp "$schema_tmp/v1/InitializeResponse.json" "$schema_dir/InitializeResponse.json" -cp "$schema_tmp/v2/ThreadBackgroundTerminalsCleanResponse.json" "$schema_dir/ThreadBackgroundTerminalsCleanResponse.json" -cp "$schema_tmp/v2/ThreadInjectItemsResponse.json" "$schema_dir/ThreadInjectItemsResponse.json" -cp "$schema_tmp/v2/ThreadStartResponse.json" "$schema_dir/ThreadStartResponse.json" -cp "$schema_tmp/v2/ThreadResumeResponse.json" "$schema_dir/ThreadResumeResponse.json" -cp "$schema_tmp/v2/TurnStartResponse.json" "$schema_dir/TurnStartResponse.json" -vp fmt "$schema_dir" +bun scripts/sync-openai-generated.mjs ``` diff --git a/src/runtimes/openai/generated-json-schema/ServerNotification.json b/src/runtimes/openai/generated-json-schema/ServerNotification.json index f7ae199..4c6bdee 100644 --- a/src/runtimes/openai/generated-json-schema/ServerNotification.json +++ b/src/runtimes/openai/generated-json-schema/ServerNotification.json @@ -459,6 +459,7 @@ "contextWindowExceeded", "sessionBudgetExceeded", "usageLimitExceeded", + "rateLimitExceeded", "serverOverloaded", "cyberPolicy", "misalignmentPolicyViolation", @@ -1272,6 +1273,94 @@ "required": ["changedPaths", "watchId"], "type": "object" }, + "FunctionCallOutputBody": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "$ref": "#/definitions/FunctionCallOutputContentItem" + }, + "type": "array" + } + ] + }, + "FunctionCallOutputContentItem": { + "description": "Responses API compatible content items that can be returned by a tool call. This is a subset of ContentItem with the types we support as function call outputs.", + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": ["input_text"], + "title": "InputTextFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "InputTextFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "image_url": { + "type": "string" + }, + "type": { + "enum": ["input_image"], + "title": "InputImageFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["image_url", "type"], + "title": "InputImageFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audio_url": { + "type": "string" + }, + "type": { + "enum": ["input_audio"], + "title": "InputAudioFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["audio_url", "type"], + "title": "InputAudioFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": ["encrypted_content"], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["encrypted_content", "type"], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" + } + ] + }, "FuzzyFileSearchMatchType": { "enum": ["file", "directory"], "type": "string" @@ -2108,6 +2197,39 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": ["string", "null"] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": ["string", "null"] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": ["plan", "default"], @@ -3377,6 +3499,30 @@ "title": "AgentMessageThreadItem", "type": "object" }, + { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": ["string", "null"] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + }, + "type": { + "enum": ["functionCallOutput"], + "title": "FunctionCallOutputThreadItemType", + "type": "string" + } + }, + "required": ["id", "name", "output", "type"], + "title": "FunctionCallOutputThreadItem", + "type": "object" + }, { "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", "properties": { @@ -4665,6 +4811,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": ["message"], diff --git a/src/runtimes/openai/generated-json-schema/ThreadResumeResponse.json b/src/runtimes/openai/generated-json-schema/ThreadResumeResponse.json index 8423516..89ca206 100644 --- a/src/runtimes/openai/generated-json-schema/ThreadResumeResponse.json +++ b/src/runtimes/openai/generated-json-schema/ThreadResumeResponse.json @@ -95,6 +95,7 @@ "contextWindowExceeded", "sessionBudgetExceeded", "usageLimitExceeded", + "rateLimitExceeded", "serverOverloaded", "cyberPolicy", "misalignmentPolicyViolation", @@ -399,6 +400,94 @@ "required": ["diff", "kind", "path"], "type": "object" }, + "FunctionCallOutputBody": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "$ref": "#/definitions/FunctionCallOutputContentItem" + }, + "type": "array" + } + ] + }, + "FunctionCallOutputContentItem": { + "description": "Responses API compatible content items that can be returned by a tool call. This is a subset of ContentItem with the types we support as function call outputs.", + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": ["input_text"], + "title": "InputTextFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "InputTextFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "image_url": { + "type": "string" + }, + "type": { + "enum": ["input_image"], + "title": "InputImageFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["image_url", "type"], + "title": "InputImageFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audio_url": { + "type": "string" + }, + "type": { + "enum": ["input_audio"], + "title": "InputAudioFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["audio_url", "type"], + "title": "InputAudioFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": ["encrypted_content"], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["encrypted_content", "type"], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" + } + ] + }, "GitInfo": { "properties": { "branch": { @@ -556,6 +645,39 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": ["string", "null"] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": ["string", "null"] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, "MultiAgentMode": { "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", "oneOf": [ @@ -1125,6 +1247,30 @@ "title": "AgentMessageThreadItem", "type": "object" }, + { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": ["string", "null"] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + }, + "type": { + "enum": ["functionCallOutput"], + "title": "FunctionCallOutputThreadItemType", + "type": "string" + } + }, + "required": ["id", "name", "output", "type"], + "title": "FunctionCallOutputThreadItem", + "type": "object" + }, { "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", "properties": { @@ -1828,6 +1974,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": ["message"], diff --git a/src/runtimes/openai/generated-json-schema/ThreadStartResponse.json b/src/runtimes/openai/generated-json-schema/ThreadStartResponse.json index dfd36f3..fa54a3d 100644 --- a/src/runtimes/openai/generated-json-schema/ThreadStartResponse.json +++ b/src/runtimes/openai/generated-json-schema/ThreadStartResponse.json @@ -95,6 +95,7 @@ "contextWindowExceeded", "sessionBudgetExceeded", "usageLimitExceeded", + "rateLimitExceeded", "serverOverloaded", "cyberPolicy", "misalignmentPolicyViolation", @@ -399,6 +400,94 @@ "required": ["diff", "kind", "path"], "type": "object" }, + "FunctionCallOutputBody": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "$ref": "#/definitions/FunctionCallOutputContentItem" + }, + "type": "array" + } + ] + }, + "FunctionCallOutputContentItem": { + "description": "Responses API compatible content items that can be returned by a tool call. This is a subset of ContentItem with the types we support as function call outputs.", + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": ["input_text"], + "title": "InputTextFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "InputTextFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "image_url": { + "type": "string" + }, + "type": { + "enum": ["input_image"], + "title": "InputImageFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["image_url", "type"], + "title": "InputImageFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audio_url": { + "type": "string" + }, + "type": { + "enum": ["input_audio"], + "title": "InputAudioFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["audio_url", "type"], + "title": "InputAudioFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": ["encrypted_content"], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["encrypted_content", "type"], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" + } + ] + }, "GitInfo": { "properties": { "branch": { @@ -556,6 +645,39 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": ["string", "null"] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": ["string", "null"] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, "MultiAgentMode": { "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", "oneOf": [ @@ -1125,6 +1247,30 @@ "title": "AgentMessageThreadItem", "type": "object" }, + { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": ["string", "null"] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + }, + "type": { + "enum": ["functionCallOutput"], + "title": "FunctionCallOutputThreadItemType", + "type": "string" + } + }, + "required": ["id", "name", "output", "type"], + "title": "FunctionCallOutputThreadItem", + "type": "object" + }, { "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", "properties": { @@ -1828,6 +1974,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": ["message"], diff --git a/src/runtimes/openai/generated-json-schema/TurnStartResponse.json b/src/runtimes/openai/generated-json-schema/TurnStartResponse.json index 36d0c3a..06bb99b 100644 --- a/src/runtimes/openai/generated-json-schema/TurnStartResponse.json +++ b/src/runtimes/openai/generated-json-schema/TurnStartResponse.json @@ -33,6 +33,7 @@ "contextWindowExceeded", "sessionBudgetExceeded", "usageLimitExceeded", + "rateLimitExceeded", "serverOverloaded", "cyberPolicy", "misalignmentPolicyViolation", @@ -337,6 +338,94 @@ "required": ["diff", "kind", "path"], "type": "object" }, + "FunctionCallOutputBody": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "$ref": "#/definitions/FunctionCallOutputContentItem" + }, + "type": "array" + } + ] + }, + "FunctionCallOutputContentItem": { + "description": "Responses API compatible content items that can be returned by a tool call. This is a subset of ContentItem with the types we support as function call outputs.", + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": ["input_text"], + "title": "InputTextFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["text", "type"], + "title": "InputTextFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "image_url": { + "type": "string" + }, + "type": { + "enum": ["input_image"], + "title": "InputImageFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["image_url", "type"], + "title": "InputImageFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audio_url": { + "type": "string" + }, + "type": { + "enum": ["input_audio"], + "title": "InputAudioFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["audio_url", "type"], + "title": "InputAudioFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": ["encrypted_content"], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": ["encrypted_content", "type"], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" + } + ] + }, "HookPromptFragment": { "properties": { "hookRunId": { @@ -480,6 +569,39 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": ["string", "null"] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": ["string", "null"] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, "NonSteerableTurnKind": { "enum": ["review", "compact"], "type": "string" @@ -656,6 +778,30 @@ "title": "AgentMessageThreadItem", "type": "object" }, + { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": ["string", "null"] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + }, + "type": { + "enum": ["functionCallOutput"], + "title": "FunctionCallOutputThreadItemType", + "type": "string" + } + }, + "required": ["id", "name", "output", "type"], + "title": "FunctionCallOutputThreadItem", + "type": "object" + }, { "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", "properties": { @@ -1259,6 +1405,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": ["message"], diff --git a/src/runtimes/openai/generated/ProtocolMethods.ts b/src/runtimes/openai/generated/ProtocolMethods.ts index f4a2116..050737b 100644 --- a/src/runtimes/openai/generated/ProtocolMethods.ts +++ b/src/runtimes/openai/generated/ProtocolMethods.ts @@ -1,6 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! -// Derived from the matching runtime JSON Schemas by scripts/prune-openai-generated-types.mjs. +// Derived from the matching runtime JSON Schemas by scripts/sync-openai-generated.mjs. export type ServerNotificationMethod = | "error" | "thread/started" diff --git a/src/runtimes/openai/generated/README.md b/src/runtimes/openai/generated/README.md index 1904cb2..d70bb37 100644 --- a/src/runtimes/openai/generated/README.md +++ b/src/runtimes/openai/generated/README.md @@ -2,24 +2,16 @@ This directory contains the transitive closure of the OpenAI app-server types imported by the driver. -They correspond to OpenAI app-server runtime version `0.150.1`, which is also pinned by the CLI dependency and container image. +They correspond to OpenAI app-server runtime version `0.151.0`, which is also pinned by the CLI dependency and container image. Runtime validation and the complete supported server method sets come from the adjacent JSON Schemas. -Regenerate those schemas first, then regenerate and prune the TypeScript output with the matching runtime: +Regenerate the selected schemas and reachable TypeScript files with the matching runtime: ```sh -set -eu -generated_dir=src/runtimes/openai/generated -generated_tmp=$(mktemp -d) -trap 'rm -rf "$generated_tmp"' EXIT -vp exec codex app-server generate-ts --experimental --out "$generated_tmp" -fd --extension ts . "$generated_dir" --exec rm -cp -R "$generated_tmp"/. "$generated_dir"/ -bun scripts/prune-openai-generated-types.mjs -vp fmt "$generated_dir" --write +bun scripts/sync-openai-generated.mjs ``` -The pruning script follows every direct generated-type import from `app-server-protocol-types.ts`, so upstream fields and their complete dependency closure are retained automatically. +The synchronization script follows every direct generated-type import from `app-server-protocol-types.ts`, so upstream fields and their complete dependency closure are retained automatically. Do not edit individual generated TypeScript files by hand. diff --git a/src/runtimes/openai/generated/v2/CyberAccessProgram.ts b/src/runtimes/openai/generated/v2/CyberAccessProgram.ts new file mode 100644 index 0000000..ccd4118 --- /dev/null +++ b/src/runtimes/openai/generated/v2/CyberAccessProgram.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Requested cyber treatment for a ChatGPT-authenticated Codex turn. + * Authorization and model-tier restrictions remain server-owned. + */ +export type CyberAccessProgram = "standard" | "daybreakBlue" | "daybreakRed"; diff --git a/src/runtimes/openai/generated/v2/ThreadResumeParams.ts b/src/runtimes/openai/generated/v2/ThreadResumeParams.ts index be72532..0c62e9a 100644 --- a/src/runtimes/openai/generated/v2/ThreadResumeParams.ts +++ b/src/runtimes/openai/generated/v2/ThreadResumeParams.ts @@ -71,7 +71,9 @@ export type ThreadResumeParams = { /** * When true, return only thread metadata and live-resume state without * populating `thread.turns`. This is useful when the client plans to call - * `thread/turns/list` immediately after resuming. + * `thread/turns/list` immediately after resuming. Full-history hydration + * is deprecated for paginated threads; use this with `thread/turns/list` + * and `thread/items/list` instead. */ excludeTurns?: boolean; /** diff --git a/src/runtimes/openai/generated/v2/TurnStartParams.ts b/src/runtimes/openai/generated/v2/TurnStartParams.ts index 17b8327..2284507 100644 --- a/src/runtimes/openai/generated/v2/TurnStartParams.ts +++ b/src/runtimes/openai/generated/v2/TurnStartParams.ts @@ -11,14 +11,22 @@ import type { JsonValue } from "../serde_json/JsonValue"; import type { AdditionalContextEntry } from "./AdditionalContextEntry"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { CyberAccessProgram } from "./CyberAccessProgram"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { TurnEnvironmentParams } from "./TurnEnvironmentParams"; +import type { TurnToolOutput } from "./TurnToolOutput"; import type { UserInput } from "./UserInput"; export type TurnStartParams = { threadId: string; clientUserMessageId?: string | null; input: Array; + /** + * Optional source classification for the caller that starts this turn. + * Ignored when this request steers an already-active turn. + */ + turnTrigger?: string | null; + toolOutput?: TurnToolOutput | null; /** * Optional metadata to enrich Codex's ResponsesAPI turn metadata. * @@ -76,6 +84,12 @@ export type TurnStartParams = { * Override the service tier for this turn and subsequent turns. */ serviceTier?: string | null | null; + /** + * Override the service tier only when this request starts a new turn. + * Use "default" for standard speed. Omitted or null inherits the thread's tier. + * Does not change the thread's tier or a turn being steered. + */ + serviceTierForTurn?: string | null; /** * Override the reasoning effort for this turn and subsequent turns. */ @@ -105,4 +119,9 @@ export type TurnStartParams = { * @deprecated Ignored. Use `effort: "ultra"` for proactive multi-agent behavior. */ multiAgentMode?: MultiAgentMode | null; + /** + * EXPERIMENTAL - Request a workspace-authorized cyber program for this + * turn. Omission preserves automatic behavior. This does not grant access. + */ + cyberAccessProgram?: CyberAccessProgram | null; }; diff --git a/src/runtimes/openai/generated/v2/TurnToolOutput.ts b/src/runtimes/openai/generated/v2/TurnToolOutput.ts new file mode 100644 index 0000000..d4e731a --- /dev/null +++ b/src/runtimes/openai/generated/v2/TurnToolOutput.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FunctionCallOutputBody } from "../FunctionCallOutputBody"; + +export type TurnToolOutput = { + name: string; + namespace: string | null; + output: FunctionCallOutputBody; +}; diff --git a/src/stores/cma-store.ts b/src/stores/cma-store.ts index 41de0db..f9b9f70 100644 --- a/src/stores/cma-store.ts +++ b/src/stores/cma-store.ts @@ -1,6 +1,11 @@ -import type { CmaInboundEvent, CmaOutboundEvent, CmaSessionStatus } from "../projections/cma"; +import type { + CmaInboundEvent, + CmaOutboundEvent, + CmaProjectedDriverCommand, + CmaSessionStatus, +} from "../projections/cma"; import type { RuntimeEventEnvelope } from "../runtime-events"; -import type { RuntimeCommand, RuntimeCommandResult } from "../runtime-command"; +import type { RuntimeCommandResult } from "../runtime-command"; export const CMA_MAX_EVENT_BYTES = 1_024 * 1_024; export const CMA_MAX_REPLAY_BYTES = 8 * CMA_MAX_EVENT_BYTES; @@ -98,7 +103,7 @@ export interface CmaSessionRecord { } export interface CmaSessionEventRecord { - readonly command: RuntimeCommand | null; + readonly command: CmaProjectedDriverCommand | null; readonly commandResult: RuntimeCommandResult | null; readonly commandStatus: "accepted" | "completed" | "failed" | null; readonly createdAt: string; @@ -145,7 +150,7 @@ export interface CmaCreateSessionInput { } export interface CmaClaimInboundEventInput { - readonly command: RuntimeCommand; + readonly command: CmaProjectedDriverCommand; readonly event: CmaInboundEvent; readonly sessionId: string; } diff --git a/src/surfaces/cma-http/contract.ts b/src/surfaces/cma-http/contract.ts index e9591e1..65f877b 100644 --- a/src/surfaces/cma-http/contract.ts +++ b/src/surfaces/cma-http/contract.ts @@ -1,5 +1,5 @@ -import type { CmaInboundEvent } from "../../projections/cma"; -import type { RuntimeCommand, RuntimeCommandResult } from "../../runtime-command"; +import type { CmaInboundEvent, CmaProjectedDriverCommand } from "../../projections/cma"; +import type { RuntimeCommandResult } from "../../runtime-command"; import type { CmaSessionRecord, CmaStore } from "../../stores/cma-store"; type HttpMethod = "DELETE" | "GET" | "POST"; @@ -22,7 +22,7 @@ export interface CmaHttpBetaHeaderRequirement { } export interface CmaHttpDriverCommandDispatchInput { - readonly command: RuntimeCommand; + readonly command: CmaProjectedDriverCommand; readonly event: CmaInboundEvent; readonly session: CmaSessionRecord; readonly signal: AbortSignal; diff --git a/src/surfaces/cma-http/request.ts b/src/surfaces/cma-http/request.ts index 689ab41..1841e9c 100644 --- a/src/surfaces/cma-http/request.ts +++ b/src/surfaces/cma-http/request.ts @@ -193,6 +193,8 @@ export async function readCmaJsonBody(request: Request): Promise { ); return JSON.parse(new TextDecoder().decode(body)) as unknown; } catch (error) { + request.signal.throwIfAborted(); + if (error instanceof CmaHttpRequestError) { throw error; } diff --git a/src/surfaces/cma-sdk/types.ts b/src/surfaces/cma-sdk/types.ts index 38c7d3a..1544b40 100644 --- a/src/surfaces/cma-sdk/types.ts +++ b/src/surfaces/cma-sdk/types.ts @@ -1,4 +1,5 @@ -import type { RuntimeCommand, RuntimeCommandResult } from "../../runtime-command"; +import type { CmaProjectedDriverCommand } from "../../projections/cma"; +import type { RuntimeCommandResult } from "../../runtime-command"; import type { CmaSessionEventRecord } from "../../stores/cma-store"; export type CmaSdkFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; @@ -27,7 +28,7 @@ export interface CmaSdkStreamOptions extends CmaSdkRequestOptions { } export interface CmaSessionEventDispatchRecord { - readonly command: RuntimeCommand; + readonly command: CmaProjectedDriverCommand; readonly event: CmaSessionEventRecord; readonly result: RuntimeCommandResult | null; readonly status: "accepted"; diff --git a/src/utils/async.ts b/src/utils/async.ts index 50f9c27..854cc4c 100644 --- a/src/utils/async.ts +++ b/src/utils/async.ts @@ -171,7 +171,7 @@ export async function readBoundedStreamBytes( signal?.removeEventListener("abort", onAbort); if (!completed) { - await cancel(); + void cancel(); } reader.releaseLock(); diff --git a/tests/acp-client-request-handler.test.ts b/tests/acp-client-request-handler.test.ts index fbbb461..4a5da6c 100644 --- a/tests/acp-client-request-handler.test.ts +++ b/tests/acp-client-request-handler.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from "bun:test"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { DriverPermissionBroker, PermissionEventDeliveryError, @@ -10,10 +14,12 @@ import { type DriverRuntimeEventPort, } from "../src/core/driver-runtime-io"; import type { DriverEventInput } from "../src/protocol/events"; +import { createDisabledLogger } from "../src/observability"; import { AcpClientRequestHandler } from "../src/runtimes/acp/acp-client-request-handler"; import { AcpAssistantTranscriptState } from "../src/runtimes/acp/acp-assistant-transcript-state"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; import { beginAcpTranscript } from "./acp-test-helpers"; +import { driverStartInput } from "./driver-boot-payload-fixture"; const BASE_HANDLER_OPTIONS = { allowedRoots: [], @@ -25,6 +31,162 @@ const BASE_HANDLER_OPTIONS = { } satisfies Omit[0], "push" | "turnEvents">; describe("ACP client request handler", () => { + test("drains a committed file report and fences writes admitted after stop", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-client-file-drain-")); + const reportEntered = Promise.withResolvers(); + const releaseReport = Promise.withResolvers(); + const handler = new AcpClientRequestHandler({ + ...BASE_HANDLER_OPTIONS, + cwd: root, + push: async () => {}, + turnEvents: new AcpAssistantTranscriptState(), + }); + const context = createAgentDriverContext({ + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, + logger: createDisabledLogger(), + payload: driverStartInput, + permission: { request: async () => "reject_once" }, + ports: { + file: { + reportChanged: async () => { + reportEntered.resolve(); + await releaseReport.promise; + }, + }, + }, + }); + const path = join(root, "committed.txt"); + + try { + await handler.initializePathScope(); + const write = handler.writeTextFile(context, { + content: "committed", + path, + sessionId: "native-session-1", + }); + await reportEntered.promise; + expect(await readFile(path, "utf8")).toBe("committed"); + + handler.beginStop(); + const drain = handler.drainFileWrites(); + let drained = false; + void drain.then(() => { + drained = true; + }); + await Bun.sleep(0); + expect(drained).toBe(false); + await expect( + handler.writeTextFile(context, { + content: "late", + path: join(root, "late.txt"), + sessionId: "native-session-1", + }), + ).rejects.toThrow("stopping"); + + releaseReport.resolve(); + await expect(Promise.all([write, drain])).resolves.toEqual([{}, undefined]); + } finally { + releaseReport.resolve(); + await handler.closePathScope(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("fails the file drain when notification fails after atomic commit", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-client-file-failure-")); + const failure = new Error("committed file report failed"); + const fatalFailures: Error[] = []; + const handler = new AcpClientRequestHandler({ + ...BASE_HANDLER_OPTIONS, + cwd: root, + onUpdateFailure: (error) => fatalFailures.push(error), + push: async () => {}, + turnEvents: new AcpAssistantTranscriptState(), + }); + const context = createAgentDriverContext({ + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, + logger: createDisabledLogger(), + payload: driverStartInput, + permission: { request: async () => "reject_once" }, + ports: { + file: { reportChanged: async () => Promise.reject(failure) }, + }, + }); + const path = join(root, "committed.txt"); + + try { + await handler.initializePathScope(); + await expect( + handler.writeTextFile(context, { + content: "committed", + path, + sessionId: "native-session-1", + }), + ).rejects.toBe(failure); + expect(await readFile(path, "utf8")).toBe("committed"); + await expect(handler.drainFileWrites()).rejects.toBe(failure); + await expect(handler.drainFileWrites()).resolves.toBeUndefined(); + expect(fatalFailures).toEqual([failure]); + } finally { + await handler.closePathScope(); + await rm(root, { force: true, recursive: true }); + } + }); + + test("does not poison turn or stop drains when a write fails before commit", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-client-file-precommit-")); + const fatalFailures: Error[] = []; + const handler = new AcpClientRequestHandler({ + ...BASE_HANDLER_OPTIONS, + cwd: root, + onUpdateFailure: (error) => fatalFailures.push(error), + push: async () => {}, + turnEvents: new AcpAssistantTranscriptState(), + }); + const context = createAgentDriverContext({ + eventSink: { + currentRunId: () => null, + pushEvents: async () => ({ accepted: [] }), + }, + logger: createDisabledLogger(), + payload: driverStartInput, + permission: { request: async () => "reject_once" }, + }); + const cancellation = new AbortController(); + const failure = new Error("pre-commit cancellation"); + + try { + await handler.initializePathScope(); + handler.openFileWriteIngress(); + cancellation.abort(failure); + await expect( + handler.writeTextFile( + context, + { + content: "not committed", + path: join(root, "cancelled.txt"), + sessionId: "native-session-1", + }, + cancellation.signal, + ), + ).rejects.toBe(failure); + handler.closeFileWriteIngress(); + await expect(handler.drainTurnFileWrites()).resolves.toBeUndefined(); + handler.beginStop(); + await expect(handler.drainFileWrites()).resolves.toBeUndefined(); + expect(fatalFailures).toEqual([]); + } finally { + await handler.closePathScope(); + await rm(root, { force: true, recursive: true }); + } + }); + test("rejects permission requests outside an active turn", async () => { let permissionRequests = 0; let pushes = 0; @@ -1009,7 +1171,7 @@ describe("ACP client request handler", () => { await expect(handler.drainPermissions()).resolves.toBeUndefined(); }); - test("fences a late cancelled permission resolution after run ownership changes", async () => { + test("closes a late cancelled permission on its captured run after ownership changes", async () => { const requestedPublishing = Promise.withResolvers(); const releaseRequested = Promise.withResolvers(); let activeRunId = "run-1"; @@ -1030,6 +1192,7 @@ describe("ACP client request handler", () => { return { accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), @@ -1079,8 +1242,8 @@ describe("ACP client request handler", () => { releaseRequested.resolve(); await drained; - expect(resolvedRunId).toBeNull(); - expect(order).toEqual(["run.cancelled"]); + expect(resolvedRunId).toBe("run-1"); + expect(order).toEqual(["permission.resolved", "run.cancelled"]); }); test("closes update ingress and drains accepted work before stopping", async () => { diff --git a/tests/acp-driver-backend.test.ts b/tests/acp-driver-backend.test.ts index 6dd997e..7782b84 100644 --- a/tests/acp-driver-backend.test.ts +++ b/tests/acp-driver-backend.test.ts @@ -11,7 +11,7 @@ import { } from "../src/core/driver-permission-broker"; import type { DriverRuntimeEventPort } from "../src/core/driver-runtime-io"; import { createDisabledLogger } from "../src/observability"; -import type { AgentDriverPermissionPort } from "../src/host-ports"; +import type { AgentDriverFilePort, AgentDriverPermissionPort } from "../src/host-ports"; import type { DriverBootPayload } from "../src/protocol/boot"; import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; @@ -29,6 +29,9 @@ const FAKE_AGENT = String.raw` const { appendFileSync, existsSync } = require("node:fs"); const { spawn } = require("node:child_process"); const logPath = process.env.TEST_LOG_PATH; +const agentPidPath = process.env.TEST_AGENT_PID_PATH; +const backpressurePath = process.env.TEST_BACKPRESSURE_PATH; +const closeWritePath = process.env.TEST_CLOSE_WRITE_PATH; const latePidPath = process.env.TEST_LATE_PID_PATH; const openCodeConfigPath = process.env.TEST_OPENCODE_CONFIG_PATH; const responsePath = process.env.TEST_RESPONSE_PATH; @@ -37,13 +40,35 @@ const triggerPath = process.env.TEST_TRIGGER_PATH; if (openCodeConfigPath) { appendFileSync(openCodeConfigPath, process.env.OPENCODE_CONFIG_CONTENT || ""); } +if (agentPidPath) appendFileSync(agentPidPath, process.pid + "\n"); let buffer = ""; +let floodTerminalId = null; let sessionReady = false; let updateSent = false; let pendingPromptId = null; +let pendingGenerationProbePromptId = null; let pendingProviderCancelPromptId = null; let pendingEndTurnPromptId = null; let resumeMetadataSent = false; +let waitingForFloodBackpressure = false; +let pendingCloseId = null; +const requestFloodOutput = (id) => + requestClient({ + id, + jsonrpc: "2.0", + method: "terminal/output", + params: { sessionId: "native-session-1", terminalId: floodTerminalId }, + }); +const sendFloodWaits = () => { + for (let index = 0; index < 9; index += 1) { + requestClient({ + id: "flood-wait-" + index, + jsonrpc: "2.0", + method: "terminal/wait_for_exit", + params: { sessionId: "native-session-1", terminalId: floodTerminalId }, + }); + } +}; const send = (message) => process.stdout.write(JSON.stringify(message) + "\n"); const requestClient = (message) => { appendFileSync(logPath, message.method + "\n"); @@ -69,6 +94,48 @@ const handle = (message) => { } } else if (message.id === "nested-wait") { appendFileSync(responsePath, JSON.stringify(message) + "\n"); + } else if (message.id === "flood-create") { + if (!message.result?.terminalId) { + throw new Error("Flood terminal creation failed."); + } + floodTerminalId = message.result.terminalId; + requestFloodOutput("flood-output-ready"); + } else if (message.id === "flood-output-ready") { + if (message.result?.output?.length < 1024 * 1024) { + setImmediate(() => requestFloodOutput("flood-output-ready")); + } else { + waitingForFloodBackpressure = true; + requestFloodOutput("flood-output-blocked"); + } + } else if (String(message.id).startsWith("flood-wait-")) { + appendFileSync(responsePath, JSON.stringify(message) + "\n"); + } else if (message.id === "generation-probe-read") { + if (message.result?.content !== "generation-0") { + throw new Error("Generation probe read failed."); + } + appendFileSync(responsePath, JSON.stringify(message) + "\n"); + send({ + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId: "native-session-1", + update: { + content: { text: "reconnected", type: "text" }, + messageId: "generation-probe-assistant", + sessionUpdate: "agent_message_chunk", + }, + }, + }); + send({ + id: pendingGenerationProbePromptId, + jsonrpc: "2.0", + result: { stopReason: "end_turn" }, + }); + pendingGenerationProbePromptId = null; + } else if (message.id === "close-write") { + appendFileSync(responsePath, JSON.stringify(message) + "\n"); + send({ id: pendingCloseId, jsonrpc: "2.0", result: {} }); + pendingCloseId = null; } return; } @@ -274,6 +341,82 @@ const handle = (message) => { }); return; } + if (message.params.prompt[0]?.text === "request-flood") { + requestClient({ + id: "flood-create", + jsonrpc: "2.0", + method: "terminal/create", + params: { + args: [ + "-e", + "process.stdout.write('x'.repeat(1024 * 1024)); setInterval(() => {}, 1000)", + ], + command: process.execPath, + outputByteLimit: 1024 * 1024, + sessionId: "native-session-1", + }, + }); + return; + } + if (message.params.prompt[0]?.text === "generation-leak") { + pendingPromptId = message.id; + for (let index = 0; index < 8; index += 1) { + requestClient({ + id: "generation-write-" + index, + jsonrpc: "2.0", + method: "fs/write_text_file", + params: { + content: "generation-" + index, + path: process.cwd() + "/generation-" + index + ".txt", + sessionId: "native-session-1", + }, + }); + } + return; + } + if (message.params.prompt[0]?.text === "generation-probe") { + pendingGenerationProbePromptId = message.id; + requestClient({ + id: "generation-probe-read", + jsonrpc: "2.0", + method: "fs/read_text_file", + params: { + path: process.cwd() + "/generation-0.txt", + sessionId: "native-session-1", + }, + }); + return; + } + if (message.params.prompt[0]?.text === "ignore-file-report") { + send({ + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId: "native-session-1", + update: { + content: { text: "written", type: "text" }, + messageId: "ignore-file-report-assistant", + sessionUpdate: "agent_message_chunk", + }, + }, + }); + requestClient({ + id: "ignored-write", + jsonrpc: "2.0", + method: "fs/write_text_file", + params: { + content: "committed", + path: process.cwd() + "/ignored-write.txt", + sessionId: "native-session-1", + }, + }); + const promptId = message.id; + setTimeout( + () => send({ id: promptId, jsonrpc: "2.0", result: { stopReason: "end_turn" } }), + 50, + ); + return; + } if (message.params.prompt[0]?.text === "many-tools") { for (let index = 0; index < 32; index += 1) { send({ @@ -368,6 +511,20 @@ const handle = (message) => { break; case "session/close": if (process.env.TEST_HANG_CLOSE === "1") return; + if (closeWritePath) { + pendingCloseId = message.id; + requestClient({ + id: "close-write", + jsonrpc: "2.0", + method: "fs/write_text_file", + params: { + content: "must not be written", + path: closeWritePath, + sessionId: "native-session-1", + }, + }); + return; + } send({ jsonrpc: "2.0", method: "session/update", @@ -386,6 +543,18 @@ const handle = (message) => { process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { buffer += chunk; + if ( + waitingForFloodBackpressure && + buffer.includes('"id":"flood-output-blocked"') + ) { + if (buffer.includes("\n")) { + throw new Error("Flood response completed before output backpressure was established."); + } + waitingForFloodBackpressure = false; + process.stdin.pause(); + appendFileSync(backpressurePath, String(Buffer.byteLength(buffer))); + sendFloodWaits(); + } for (let newline; (newline = buffer.indexOf("\n")) >= 0; ) { const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); @@ -447,6 +616,7 @@ async function createHarness( readonly authenticate?: boolean; readonly blockResume?: boolean; readonly failResume?: boolean; + readonly file?: AgentDriverFilePort; readonly hangClose?: boolean; readonly metadataOnResume?: boolean; readonly openCodeInstructions?: boolean; @@ -454,9 +624,13 @@ async function createHarness( readonly permission?: AgentDriverPermissionPort["request"]; readonly spawnLateChild?: boolean; readonly updateBeforeSessionResponse?: boolean; + readonly writeOnClose?: boolean; } = {}, ) { const root = await mkdtemp(join(tmpdir(), "driver-acp-backend-")); + const agentPidPath = join(root, "agent.pid"); + const backpressurePath = join(root, "backpressure.log"); + const closeWritePath = join(root, "close-write.txt"); const logPath = join(root, "methods.log"); const latePidPath = join(root, "late.pid"); const openCodeConfigPath = join(root, "opencode-config.json"); @@ -475,6 +649,8 @@ async function createHarness( environment: { variables: { ...(options.openCodeInstructions ? { OPENCODE_CONFIG_CONTENT: "{}" } : {}), + TEST_AGENT_PID_PATH: agentPidPath, + TEST_BACKPRESSURE_PATH: backpressurePath, TEST_LOG_PATH: logPath, TEST_LATE_PID_PATH: latePidPath, TEST_OPENCODE_CONFIG_PATH: openCodeConfigPath, @@ -482,6 +658,7 @@ async function createHarness( TEST_RESUME_GATE_PATH: resumeGatePath, TEST_TRIGGER_PATH: triggerPath, TEST_BLOCK_RESUME: options.blockResume ? "1" : "0", + TEST_CLOSE_WRITE_PATH: options.writeOnClose ? closeWritePath : "", TEST_FAIL_RESUME: options.failResume ? "1" : "0", TEST_HANG_CLOSE: options.hangClose ? "1" : "0", TEST_METADATA_ON_RESUME: options.metadataOnResume ? "1" : "0", @@ -552,7 +729,13 @@ async function createHarness( options.onEvents?.(events); return { - accepted: events.map((event) => ({ seq: ++acceptedSeq, type: event.kind })), + accepted: events.map((event) => { + const eventId = event.sourceEventId ?? event.id; + if (eventId === undefined) { + throw new Error("Test event is missing its source identity."); + } + return { eventId, seq: ++acceptedSeq, type: event.kind }; + }), }; }, }, @@ -566,6 +749,7 @@ async function createHarness( payload, permission: { request: options.permission ?? (async () => "reject_once") }, ports: { + ...(options.file === undefined ? {} : { file: options.file }), skill: { materialize: async () => [] }, }, }); @@ -591,8 +775,29 @@ async function createHarness( } } + const handleInput = backend.handleInput.bind(backend); + backend.handleInput = async (inputContext, input, runId, signal) => { + activeRunId ??= runId; + + try { + await handleInput(inputContext, input, runId, signal); + } finally { + if (activeRunId === runId) { + activeRunId = null; + } + } + }; + return { + agentPidPath, backend, + backpressurePath, + beginRun(runId: RunId) { + if (activeRunId !== null) { + throw new Error(`Test driver run ${activeRunId} is already active.`); + } + activeRunId = runId; + }, blockNext(kind: string) { const entered = Promise.withResolvers(); const release = Promise.withResolvers(); @@ -600,6 +805,7 @@ async function createHarness( return { entered: entered.promise, release: release.resolve }; }, context, + closeWritePath, async destroy() { block?.release.reject(new Error("test cleanup")); await backend.stop(context, "test cleanup", new AbortController().signal).catch(() => {}); @@ -731,6 +937,267 @@ describe("ACP driver backend lifecycle", () => { } }); + test("fails closed when ACP client request capacity is exhausted", async () => { + const harness = await createHarness(); + let replacement: Awaited> | null = null; + + try { + const settlement = await settlePromiseWithTimeout( + harness.backend.handleInput( + harness.context, + { text: "request-flood" }, + DRIVER_TEST_IDS.runId as RunId, + ), + { label: "backpressured ACP capacity close", timeoutMs: 4_000 }, + ); + expect(settlement).toMatchObject({ + error: expect.objectContaining({ + message: expect.stringContaining("ACP client request capacity exceeded"), + }), + status: "failed", + }); + expect( + (await harness.methods()).filter((method) => method === "terminal/wait_for_exit"), + ).toHaveLength(9); + const bufferedBytes = Number.parseInt(await readFile(harness.backpressurePath, "utf8"), 10); + expect(bufferedBytes).toBeGreaterThan(0); + expect(bufferedBytes).toBeLessThan(1024 * 1024); + const [agentPid] = (await readFile(harness.agentPidPath, "utf8")).trim().split("\n"); + expect(isRunning(Number.parseInt(agentPid!, 10))).toBe(false); + + replacement = await createHarness(); + await expect( + replacement.backend.handleInput( + replacement.context, + { text: "after backpressure" }, + DRIVER_TEST_IDS.runId as RunId, + ), + ).resolves.toBeUndefined(); + } finally { + await replacement?.destroy(); + await harness.destroy(); + } + }, 10_000); + + test("drains accepted file requests before recycling their connection", async () => { + const blockedReports = Promise.withResolvers(); + const reportsEntered = Promise.withResolvers(); + let reportCount = 0; + const harness = await createHarness({ + file: { + reportChanged: async (_change, _signal) => { + reportCount += 1; + if (reportCount === 8) { + reportsEntered.resolve(); + } + await blockedReports.promise; + }, + }, + }); + + try { + const abandoned = harness.backend.handleInput( + harness.context, + { text: "generation-leak" }, + DRIVER_TEST_IDS.runId as RunId, + ); + void abandoned.catch(() => {}); + await reportsEntered.promise; + await harness.backend.cancelActiveTurn(harness.context, "test generation recycle"); + expect( + await settlePromiseWithTimeout(abandoned, { + label: "cancelled ACP file request drain", + timeoutMs: 50, + }), + ).toMatchObject({ status: "timed_out" }); + blockedReports.resolve(); + await expect(abandoned).rejects.toThrow("cancelled"); + + const probe = harness.backend.handleInput( + harness.context, + { text: "generation-probe" }, + DRIVER_TEST_IDS.secondRunId as RunId, + ); + void probe.catch(() => {}); + await waitForAcpTestCondition( + async () => + (await harness.responses()).some( + (response) => response["id"] === "generation-probe-read", + ), + "recycled ACP client response", + ); + await probe; + } finally { + blockedReports.resolve(); + await new Promise((resolve) => setImmediate(resolve)); + await harness.destroy(); + } + }); + + test("drains committed file reports before full-stop process cleanup", async () => { + const blockedReports = Promise.withResolvers(); + const reportsEntered = Promise.withResolvers(); + const processStopEntered = Promise.withResolvers(); + const originalStop = acpAgentProcess.stopAcpAgentProcess; + const stopSpy = spyOn(acpAgentProcess, "stopAcpAgentProcess").mockImplementation( + async (...args) => { + processStopEntered.resolve(); + await originalStop(...args); + }, + ); + let reportCount = 0; + let harness: Awaited> | null = null; + + try { + harness = await createHarness({ + file: { + reportChanged: async () => { + reportCount += 1; + if (reportCount === 8) { + reportsEntered.resolve(); + } + await blockedReports.promise; + }, + }, + }); + const input = harness.backend.handleInput( + harness.context, + { text: "generation-leak" }, + DRIVER_TEST_IDS.runId as RunId, + ); + void input.catch(() => {}); + await reportsEntered.promise; + + const stop = harness.backend.stop( + harness.context, + "test committed file drain", + new AbortController().signal, + ); + void stop.catch(() => {}); + expect( + await settlePromiseWithTimeout(processStopEntered.promise, { + label: "ACP process stop before file drain", + timeoutMs: 50, + }), + ).toMatchObject({ status: "timed_out" }); + + blockedReports.resolve(); + await expect(stop).resolves.toBeUndefined(); + await expect(processStopEntered.promise).resolves.toBeUndefined(); + expect(reportCount).toBe(8); + await Promise.allSettled([input]); + } finally { + blockedReports.resolve(); + await harness?.destroy(); + stopSpy.mockRestore(); + } + }); + + test("fails the turn before its terminal when a provider ignores a committed file report", async () => { + const failure = new Error("committed file.changed failed"); + const reportEntered = Promise.withResolvers(); + const releaseReport = Promise.withResolvers(); + const harness = await createHarness({ + file: { + reportChanged: async () => { + reportEntered.resolve(); + await releaseReport.promise; + throw failure; + }, + }, + }); + + try { + const input = harness.backend.handleInput( + harness.context, + { text: "ignore-file-report" }, + DRIVER_TEST_IDS.runId as RunId, + ); + void input.catch(() => {}); + await reportEntered.promise; + expect( + await settlePromiseWithTimeout(input, { + label: "ACP committed file terminal fence", + timeoutMs: 50, + }), + ).toMatchObject({ status: "timed_out" }); + + releaseReport.resolve(); + expect( + await settlePromiseWithTimeout(input, { + label: "ACP committed file turn failure", + timeoutMs: 2_000, + }), + ).toEqual({ error: failure, status: "failed" }); + expect( + await settlePromiseWithTimeout(harness.lifecycleFailure, { + label: "ACP committed file lifecycle failure", + timeoutMs: 2_000, + }), + ).toEqual({ status: "completed", value: failure }); + expect( + harness.events + .filter( + (event) => + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed", + ) + .map((event) => event.kind), + ).toEqual(["run.failed"]); + await expect( + harness.backend.handleInput( + harness.context, + { text: "must not continue" }, + DRIVER_TEST_IDS.secondRunId as RunId, + ), + ).rejects.toThrow("connection is not initialized"); + await expect( + harness.backend.stop( + harness.context, + "test committed file failure cleanup", + new AbortController().signal, + ), + ).resolves.toBeUndefined(); + await expect( + harness.backend.stop( + harness.context, + "test committed file cleanup join", + new AbortController().signal, + ), + ).resolves.toBeUndefined(); + } finally { + releaseReport.resolve(); + await harness.destroy(); + } + }); + + test("rejects a file write arriving after the full-stop ingress fence", async () => { + const harness = await createHarness({ writeOnClose: true }); + + try { + await expect( + harness.backend.stop( + harness.context, + "test close write fence", + new AbortController().signal, + ), + ).resolves.toBeUndefined(); + + expect(await harness.responses()).toContainEqual( + expect.objectContaining({ + error: expect.any(Object), + id: "close-write", + }), + ); + await expect(readFile(harness.closeWritePath, "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); + } finally { + await harness.destroy(); + } + }); + test("projects an active transport loss as a failed turn", async () => { const harness = await createHarness(); @@ -1594,6 +2061,7 @@ describe("ACP driver backend lifecycle", () => { } return { accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), @@ -1655,6 +2123,7 @@ describe("ACP driver backend lifecycle", () => { acceptedKinds.push(...events.map((event) => event.kind)); return { accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), @@ -2017,7 +2486,7 @@ describe("ACP driver backend lifecycle", () => { } }); - test("fails an active turn after stop cleanup rejects and lets a later stop retry", async () => { + test("joins a successful stop retry before settling the active turn", async () => { const harness = await createHarness(); try { @@ -2055,7 +2524,7 @@ describe("ACP driver backend lifecycle", () => { new AbortController().signal, ); gate.release(); - await expect(input).rejects.toThrow("cancelled turn process recycle failed"); + await expect(input).rejects.toThrow("cancelled"); expect( harness.events .filter( @@ -2065,7 +2534,7 @@ describe("ACP driver backend lifecycle", () => { event.kind === "run.failed", ) .map((event) => event.kind), - ).toEqual(["run.failed"]); + ).toEqual(["run.cancelled"]); await expect(retryStop).resolves.toBeUndefined(); } finally { await harness.destroy(); diff --git a/tests/acp-event-translator-lifecycle.test.ts b/tests/acp-event-translator-lifecycle.test.ts index 237f6d6..8bb0e8d 100644 --- a/tests/acp-event-translator-lifecycle.test.ts +++ b/tests/acp-event-translator-lifecycle.test.ts @@ -385,6 +385,16 @@ describe("ACP runtime event translation", () => { event.kind === "message.added" && eventPayload(event)["messageId"] === finalMessageId, )?.payload, ).toMatchObject({ content: finalText }); + const finalSnapshotIndex = events.findIndex( + (event) => + event.kind === "message.added" && eventPayload(event)["messageId"] === finalMessageId, + ); + const finalSealIndex = events.findIndex( + (event) => + event.kind === "message.completed" && eventPayload(event)["messageId"] === finalMessageId, + ); + expect(finalSealIndex).toBeGreaterThan(finalSnapshotIndex); + expect(events.indexOf(completed)).toBeGreaterThan(finalSealIndex); }); test("evicts bounded settled assistant IDs without suppressing the oldest message", () => { @@ -610,6 +620,7 @@ describe("ACP runtime event translation", () => { expect(eventPayload(anonymousFailed)).toMatchObject({ error: { code: "acp.empty_turn", + retryable: true, }, recoverable: true, stopReason: "end_turn", @@ -1097,5 +1108,13 @@ describe("ACP runtime event translation", () => { { kind: "allow_once", name: "Allow once", optionId: "allow" }, { kind: "reject_once", name: "Reject once", optionId: "reject" }, ]); + + expect( + toPermissionRequest({ + params: { toolCall: { kind: "shell", title: "Run command" } }, + requestId: "rpc-fallback", + runId: RUN_ID, + }).request.toolCallId, + ).toBe("rpc-fallback"); }); }); diff --git a/tests/acp-event-translator-session-update.test.ts b/tests/acp-event-translator-session-update.test.ts index 5a4e257..0090297 100644 --- a/tests/acp-event-translator-session-update.test.ts +++ b/tests/acp-event-translator-session-update.test.ts @@ -143,6 +143,7 @@ describe("ACP runtime event translation", () => { error: { code: "acp.max_turn_requests", message: "ACP prompt stopped with max_turn_requests.", + retryable: false, }, recoverable: false, stopReason: "max_turn_requests", @@ -208,6 +209,7 @@ describe("ACP runtime event translation", () => { error: { code: "acp.empty_turn", message: "ACP prompt ended without assistant output or tool activity.", + retryable: true, }, recoverable: true, stopReason: "end_turn", @@ -416,7 +418,7 @@ describe("ACP runtime event translation", () => { "run.failed", ]); expect(eventPayload(requireEvent(events, "run.failed"))).toMatchObject({ - error: { message: "transport closed" }, + error: { message: "transport closed", retryable: false }, }); expect(eventPayload(events[5]!)).not.toHaveProperty("error"); expect(eventPayload(events[6]!)).not.toHaveProperty("error"); diff --git a/tests/acp-file-system.test.ts b/tests/acp-file-system.test.ts index 26d4042..bf555c5 100644 --- a/tests/acp-file-system.test.ts +++ b/tests/acp-file-system.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, spyOn, test } from "bun:test"; import { truncateSync } from "node:fs"; import { chmod, @@ -21,11 +21,13 @@ import { join } from "node:path"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; +import type { AgentDriverFilePort } from "../src/host-ports"; import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; import { AcpFileSystem } from "../src/runtimes/acp/acp-file-system"; import { AcpPathScope } from "../src/runtimes/acp/acp-path-scope"; +import { settlePromiseWithTimeout } from "../src/utils/async"; import { driverStartInput } from "./driver-boot-payload-fixture"; const pathScopes = new Set(); @@ -46,7 +48,10 @@ afterEach(async () => { pathScopes.clear(); }); -function createContext(events: DriverEventInput[]): AgentDriverContext { +function createContext( + events: DriverEventInput[], + reportChanged?: AgentDriverFilePort["reportChanged"], +): AgentDriverContext { return createAgentDriverContext({ eventSink: { currentRunId: () => null, @@ -54,6 +59,7 @@ function createContext(events: DriverEventInput[]): AgentDriverContext { events.push(...input.events); return { accepted: input.events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), @@ -65,6 +71,7 @@ function createContext(events: DriverEventInput[]): AgentDriverContext { permission: { request: async () => "reject_once", }, + ...(reportChanged === undefined ? {} : { ports: { file: { reportChanged } } }), }); } @@ -136,6 +143,61 @@ describe("ACP file system bridge", () => { } }); + test("closes the write directory before bounded committed-file reporting", async () => { + const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-report-")); + const directory = join(root, "nested"); + const path = join(directory, "note.txt"); + const reportEntered = Promise.withResolvers(); + const releaseReport = Promise.withResolvers(); + const reportDeadline = new AbortController(); + const timeout = spyOn(AbortSignal, "timeout").mockReturnValue(reportDeadline.signal); + const request = new AbortController(); + let directoryHandleOpen = true; + let receivedSignal: AbortSignal | undefined; + const context = createContext([], async (_change, signal) => { + receivedSignal = signal; + const directoryPath = await realpath(directory); + const openPaths = await Promise.all( + (await readdir(`/proc/${process.pid}/fd`)).map((fd) => + readlink(`/proc/${process.pid}/fd/${fd}`).catch(() => ""), + ), + ); + directoryHandleOpen = openPaths.includes(directoryPath); + reportEntered.resolve(); + await releaseReport.promise; + }); + const reportAbort = new Error("file report deadline"); + + try { + const write = createFileSystem(root).writeTextFile( + context, + { content: "committed", path }, + request.signal, + ); + await reportEntered.promise; + + expect(directoryHandleOpen).toBe(false); + expect(receivedSignal).toBe(reportDeadline.signal); + expect(receivedSignal).not.toBe(request.signal); + request.abort(new Error("late turn cancellation")); + expect( + await settlePromiseWithTimeout(write, { + label: "blocked ACP file report", + timeoutMs: 10, + }), + ).toMatchObject({ status: "timed_out" }); + + reportDeadline.abort(reportAbort); + await expect(write).rejects.toBe(reportAbort); + expect(await readFile(path, "utf8")).toBe("committed"); + } finally { + reportDeadline.abort(reportAbort); + releaseReport.resolve(); + timeout.mockRestore(); + await rm(root, { force: true, recursive: true }); + } + }); + test("rejects files above the deployment byte limit before reading them", async () => { const root = await mkdtemp(join(tmpdir(), "driver-acp-fs-limit-")); const path = join(root, "large.txt"); diff --git a/tests/acp-terminal-manager.test.ts b/tests/acp-terminal-manager.test.ts index af0b98b..c25d6b2 100644 --- a/tests/acp-terminal-manager.test.ts +++ b/tests/acp-terminal-manager.test.ts @@ -8,6 +8,7 @@ import { join } from "node:path"; import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { createDriverId, isDriverId } from "../src/protocol/id"; +import { DriverEventRejectedError } from "../src/core/driver-runtime-io"; import { spawnLinuxProcessTreeWatchdog } from "../src/runtimes/child-process"; import { AcpPathScope } from "../src/runtimes/acp/acp-path-scope"; import { AcpTerminalManager } from "../src/runtimes/acp/acp-terminal-manager"; @@ -892,6 +893,257 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } }); + test("coalesces concurrent and repeated terminal kills into one durable event", async () => { + const killPublishing = Promise.withResolvers(); + const allowKill = Promise.withResolvers(); + let killPushes = 0; + const harness = createHarness(async (reason) => { + if (reason === "driver.acp.terminal.killed") { + killPushes += 1; + killPublishing.resolve(); + await allowKill.promise; + } + }); + + try { + const terminal = await harness.manager.create(harness.context, { + args: ["-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)'], + command: process.execPath, + }); + await waitForTerminalOutput(harness.manager, terminal); + + const first = harness.manager.kill(harness.context, terminal); + await killPublishing.promise; + const second = harness.manager.kill(harness.context, terminal); + allowKill.resolve(); + + await expect(Promise.all([first, second])).resolves.toEqual([{}, {}]); + await expect(harness.manager.kill(harness.context, terminal)).resolves.toEqual({}); + expect(killPushes).toBe(1); + expect(harness.events.filter((event) => event.kind === "terminal.killed")).toHaveLength(1); + + await harness.manager.release(harness.context, terminal); + } finally { + allowKill.resolve(); + await harness.manager.stopAll(harness.context).catch(() => {}); + } + }); + + test.each(["release", "stopAll"] as const)( + "retains a terminal when a concurrent %s observes failed kill publication", + async (action) => { + let killedDraft: DriverEventInput | undefined; + let killPushes = 0; + const harness = createHarness( + async (reason, events) => { + if (reason !== "driver.acp.terminal.killed") { + return; + } + + const draft = events[0]!; + killedDraft ??= draft; + expect(draft).toBe(killedDraft); + killPushes += 1; + if (killPushes === 1) { + throw new Error("kill publication failed"); + } + }, + 1, + true, + ); + + try { + const terminal = await harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }); + await harness.manager.waitForExit(terminal); + const firstKill = harness.manager.kill(harness.context, terminal); + const secondKill = harness.manager.kill(harness.context, terminal); + const cleanup = + action === "release" + ? harness.manager.release(harness.context, terminal) + : harness.manager.stopAll(harness.context); + const results = await Promise.allSettled([firstKill, secondKill, cleanup]); + + expect(results.map((result) => result.status)).toEqual([ + "rejected", + "rejected", + "rejected", + ]); + expect(killPushes).toBe(1); + expect(() => harness.manager.output(terminal)).not.toThrow(); + expect(harness.events.some((event) => event.kind === "terminal.killed")).toBe(false); + expect(harness.events.some((event) => event.kind === "terminal.released")).toBe(false); + if (action === "release") { + await expect( + harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }), + ).rejects.toThrow("terminal limit of 1 is exhausted"); + } + + await expect( + action === "release" + ? harness.manager.release(harness.context, terminal).then(() => {}) + : harness.manager.stopAll(harness.context), + ).resolves.toBeUndefined(); + expect(killPushes).toBe(2); + expect( + harness.events + .filter((event) => + [ + "terminal.created", + "terminal.exited", + "terminal.killed", + "terminal.released", + ].includes(event.kind), + ) + .map((event) => event.kind), + ).toEqual(["terminal.created", "terminal.exited", "terminal.killed", "terminal.released"]); + expect(() => harness.manager.output(terminal)).toThrow("does not exist"); + } finally { + await harness.manager.stopAll(harness.context).catch(() => {}); + } + }, + ); + + test("retains a terminal until its exit event is durably published", async () => { + let exitDraft: DriverEventInput | undefined; + let exitPushes = 0; + let failExit = true; + const harness = createHarness( + async (reason, events) => { + if (reason !== "driver.acp.terminal.exited") { + return; + } + + const draft = events[0]!; + exitDraft ??= draft; + expect(draft).toBe(exitDraft); + exitPushes += 1; + if (failExit) { + throw new Error("exit publication failed"); + } + }, + 1, + true, + ); + + try { + const terminal = await harness.manager.create(harness.context, { + args: ["-e", "process.stdout.write(String(process.pid));setInterval(() => {}, 1000)"], + command: process.execPath, + }); + await waitForTerminalOutput(harness.manager, terminal, "non-empty"); + process.kill(Number.parseInt(harness.manager.output(terminal).output, 10), "SIGTERM"); + await harness.manager.waitForExit(terminal); + await Bun.sleep(0); + + await expect(harness.manager.release(harness.context, terminal)).rejects.toThrow( + "exit publication failed", + ); + expect(exitPushes).toBe(2); + expect(() => harness.manager.output(terminal)).not.toThrow(); + expect(harness.events.some((event) => event.kind === "terminal.exited")).toBe(false); + expect(harness.events.some((event) => event.kind === "terminal.released")).toBe(false); + + failExit = false; + await expect(harness.manager.release(harness.context, terminal)).resolves.toEqual({}); + expect(exitPushes).toBe(3); + expect( + harness.events + .filter((event) => + ["terminal.created", "terminal.exited", "terminal.released"].includes(event.kind), + ) + .map((event) => event.kind), + ).toEqual(["terminal.created", "terminal.exited", "terminal.released"]); + expect(() => harness.manager.output(terminal)).toThrow("does not exist"); + } finally { + failExit = false; + await harness.manager.stopAll(harness.context).catch(() => {}); + } + }); + + test("keeps a kill admitted before release ahead of the released event", async () => { + const killPublishing = Promise.withResolvers(); + const allowKill = Promise.withResolvers(); + const harness = createHarness( + async (reason) => { + if (reason === "driver.acp.terminal.killed") { + killPublishing.resolve(); + await allowKill.promise; + } + }, + undefined, + true, + ); + + try { + const terminal = await harness.manager.create(harness.context, { + args: ["-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)'], + command: process.execPath, + }); + await waitForTerminalOutput(harness.manager, terminal); + + const kill = harness.manager.kill(harness.context, terminal); + await killPublishing.promise; + const release = harness.manager.release(harness.context, terminal); + expect(() => harness.manager.output(terminal)).toThrow("does not exist"); + await Bun.sleep(0); + expect(harness.events.some((event) => event.kind === "terminal.released")).toBe(false); + + allowKill.resolve(); + await expect(Promise.all([kill, release])).resolves.toEqual([{}, {}]); + expect( + harness.events + .filter((event) => event.kind === "terminal.killed" || event.kind === "terminal.released") + .map((event) => event.kind), + ).toEqual(["terminal.killed", "terminal.released"]); + } finally { + allowKill.resolve(); + await harness.manager.stopAll(harness.context).catch(() => {}); + } + }); + + test("invalidates every public terminal operation while release is in flight", async () => { + const releasePublishing = Promise.withResolvers(); + const allowRelease = Promise.withResolvers(); + const harness = createHarness(async (reason) => { + if (reason === "driver.acp.terminal.released") { + releasePublishing.resolve(); + await allowRelease.promise; + } + }); + + try { + const terminal = await harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }); + await harness.manager.waitForExit(terminal); + const release = harness.manager.release(harness.context, terminal); + await releasePublishing.promise; + + expect(() => harness.manager.output(terminal)).toThrow("does not exist"); + for (const operation of [ + () => harness.manager.waitForExit(terminal), + () => harness.manager.kill(harness.context, terminal), + () => harness.manager.release(harness.context, terminal), + ]) { + await expect(operation()).rejects.toThrow("does not exist"); + } + + allowRelease.resolve(); + await expect(release).resolves.toEqual({}); + expect(harness.events.some((event) => event.kind === "terminal.killed")).toBe(false); + } finally { + allowRelease.resolve(); + await harness.manager.stopAll(harness.context).catch(() => {}); + } + }); + test("aborts an SDK terminal wait without abandoning the terminal", async () => { const harness = createHarness(); @@ -912,6 +1164,57 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } }); + test("aborts an SDK terminal kill without abandoning cleanup ownership", async () => { + const cleanup = Promise.withResolvers(); + const harness = createHarness(undefined, undefined, false, fakeWatchdog(cleanup.promise)); + + try { + const terminal = await harness.manager.create(harness.context, { + args: ["-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)'], + command: process.execPath, + }); + await waitForTerminalOutput(harness.manager, terminal); + const controller = new AbortController(); + const killed = harness.manager.kill(harness.context, terminal, controller.signal); + controller.abort(); + + await expect(killed).rejects.toBeInstanceOf(DOMException); + expect(() => harness.manager.output(terminal)).not.toThrow(); + expect(harness.events.some((event) => event.kind === "terminal.killed")).toBe(false); + + await expect(harness.manager.release(harness.context, terminal)).resolves.toEqual({}); + expect(() => harness.manager.output(terminal)).toThrow("does not exist"); + } finally { + cleanup.resolve(); + await harness.manager.stopAll(harness.context).catch(() => {}); + } + }); + + test("bounds an SDK terminal kill and retains cleanup ownership after timeout", async () => { + const cleanup = Promise.withResolvers(); + const harness = createHarness(undefined, undefined, false, fakeWatchdog(cleanup.promise)); + + try { + const terminal = await harness.manager.create(harness.context, { + args: ["-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)'], + command: process.execPath, + }); + await waitForTerminalOutput(harness.manager, terminal); + + await expect(harness.manager.kill(harness.context, terminal)).rejects.toThrow( + "cleanup did not finish within 4000ms after force kill", + ); + expect(() => harness.manager.output(terminal)).not.toThrow(); + expect(harness.events.some((event) => event.kind === "terminal.killed")).toBe(false); + + await expect(harness.manager.release(harness.context, terminal)).resolves.toEqual({}); + expect(() => harness.manager.output(terminal)).toThrow("does not exist"); + } finally { + cleanup.resolve(); + await harness.manager.stopAll(harness.context).catch(() => {}); + } + }, 8_000); + test.each([ ["cooperative", "", "SIGTERM"], ["TERM-resistant", 'process.on("SIGTERM", () => {});', "SIGKILL"], @@ -998,42 +1301,166 @@ while (!existsSync(${JSON.stringify(workerPidPath)})) Atomics.wait(sleeper, 0, 0 } }); - test("retains a terminal for shutdown retry when failed creation cleanup cannot confirm exit", async () => { - const cleanup = Promise.withResolvers(); + test("closes an ACK-unknown creation with the same durable identity", async () => { + const directory = await mkdtemp(join(tmpdir(), "mosoo-acp-created-ack-")); + const pidPath = join(directory, "terminal.pid"); + let createdDraft: DriverEventInput | undefined; + let createdPushes = 0; + let releaseDraft: DriverEventInput | undefined; + let releasePushes = 0; const harness = createHarness( - async (reason) => { + async (reason, events) => { if (reason === "driver.acp.terminal.created") { - throw new Error("event sink unavailable"); + if (createdDraft === undefined) { + createdDraft = events[0]; + createdPushes += 1; + throw new Error("creation receipt lost after commit"); + } + if (events[0]?.sourceEventId === createdDraft.sourceEventId) { + expect(events[0]).toBe(createdDraft); + createdPushes += 1; + } + } + if (reason === "driver.acp.terminal.released") { + releaseDraft ??= events[0]; + expect(events[0]).toBe(releaseDraft); + if (releasePushes++ === 0) { + throw new Error("release receipt lost after commit"); + } } }, - undefined, + 1, false, - fakeWatchdog(cleanup.promise), ); try { await expect( harness.manager.create(harness.context, { - args: ["-e", "setInterval(() => {}, 1000);"], + args: [ + "-e", + `require("node:fs").writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));setInterval(() => {}, 1000);`, + ], command: process.execPath, }), - ).rejects.toThrow(); - const terminalId = ( - harness.events.find((event) => event.kind === "terminal.created")?.payload as - | Record - | undefined - )?.["terminalId"]; + ).rejects.toThrow("creation cleanup failed"); + const pid = await waitForPidFile(pidPath); + await waitForProcessExit(pid); - expect(terminalId).toBeString(); + expect(createdPushes).toBe(2); + expect(createdDraft?.sourceEventId).toMatch(/^acp\.terminal\.created:/); + expect(releasePushes).toBe(1); + const terminalId = (createdDraft!.payload as { terminalId: string }).terminalId; expect(() => harness.manager.output({ terminalId })).not.toThrow(); - + await expect( + harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }), + ).rejects.toThrow("terminal limit of 1 is exhausted"); + expect( + [...new Map(harness.events.map((event) => [event.sourceEventId, event])).values()].map( + (event) => event.kind, + ), + ).toEqual(["terminal.created", "terminal.exited", "terminal.released"]); await expect(harness.manager.stopAll(harness.context)).resolves.toBeUndefined(); + expect(releasePushes).toBe(2); expect(() => harness.manager.output({ terminalId })).toThrow("does not exist"); } finally { - cleanup.resolve(); + await harness.manager.stopAll(harness.context).catch(() => {}); + await rm(directory, { force: true, recursive: true }); + } + }); + + test("cleans a creation that the durable sink rejects before commit", async () => { + let rejectCreated = true; + const harness = createHarness( + async (reason, events) => { + if (rejectCreated && reason === "driver.acp.terminal.created") { + throw new DriverEventRejectedError( + events[0]!.sourceEventId!, + new Error("creation rejected before commit"), + ); + } + }, + 1, + true, + ); + + try { + await expect( + harness.manager.create(harness.context, { + args: ["-e", "setInterval(() => {}, 1000)"], + command: process.execPath, + }), + ).rejects.toThrow("creation rejected before commit"); + expect(harness.events).toHaveLength(0); + + rejectCreated = false; + const next = await harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }); + await harness.manager.waitForExit(next); + await harness.manager.release(harness.context, next); + } finally { + rejectCreated = false; await harness.manager.stopAll(harness.context).catch(() => {}); } - }, 5_000); + }); + + test("treats a mismatched rejection identity as an ACK-unknown creation", async () => { + let createdDraft: DriverEventInput | undefined; + let createdPushes = 0; + const harness = createHarness( + async (reason, events) => { + if (reason !== "driver.acp.terminal.created") { + return; + } + if (createdPushes >= 2) { + return; + } + + createdDraft ??= events[0]; + expect(events[0]).toBe(createdDraft); + createdPushes += 1; + if (createdPushes === 1) { + throw new DriverEventRejectedError( + "different-source-event", + new Error("different creation rejected"), + ); + } + }, + 1, + false, + ); + + try { + await expect( + harness.manager.create(harness.context, { + args: ["-e", "setInterval(() => {}, 1000)"], + command: process.execPath, + }), + ).rejects.toThrow("different creation rejected"); + + expect(createdPushes).toBe(2); + expect( + [...new Map(harness.events.map((event) => [event.sourceEventId, event])).values()].map( + (event) => event.kind, + ), + ).toEqual(["terminal.created", "terminal.exited", "terminal.released"]); + const terminalId = (createdDraft!.payload as { terminalId: string }).terminalId; + expect(() => harness.manager.output({ terminalId })).toThrow("does not exist"); + + const next = await harness.manager.create(harness.context, { + args: ["-e", "process.exit(0)"], + command: process.execPath, + }); + await harness.manager.waitForExit(next); + await harness.manager.release(harness.context, next); + } finally { + await harness.manager.stopAll(harness.context).catch(() => {}); + } + }); test("stopAll joins an in-progress release and emits it once", async () => { const releasePublishing = Promise.withResolvers(); diff --git a/tests/agent-driver-kernel-async-queue.test.ts b/tests/agent-driver-kernel-async-queue.test.ts index ce0c0d2..532a233 100644 --- a/tests/agent-driver-kernel-async-queue.test.ts +++ b/tests/agent-driver-kernel-async-queue.test.ts @@ -7,7 +7,12 @@ import type { DriverEventInput } from "../src/protocol/events"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; import type { RuntimeCommand } from "../src/runtime-command"; import { driverBootPayload } from "./driver-boot-payload-fixture"; -import { DRIVER_TEST_IDS, bootPayload, createBackend } from "./driver-runtime-boundary-fixtures"; +import { + DRIVER_TEST_IDS, + bootPayload, + createBackend, + settleBackendInput, +} from "./driver-runtime-boundary-fixtures"; const jsonBytes = (value: unknown) => Buffer.byteLength(JSON.stringify(value), "utf8"); @@ -161,6 +166,7 @@ describe("AgentDriverKernelCore", () => { delivery: "lossless", kind: "diagnostic.reported", payload: { message: "" }, + sourceEventId: "large-lossless-event", }; const event: DriverEventInput = { ...base, @@ -187,7 +193,7 @@ describe("AgentDriverKernelCore", () => { await expect(events.next()).resolves.toEqual({ done: true, value: undefined }); }); - test("can retry a run terminal after an oversized reserve admission fails", async () => { + test("omits an oversized run error before terminal queue admission", async () => { const kernel = new AgentDriverKernelCore({ backendFactory: () => createBackend() }); const events = kernel.events()[Symbol.asyncIterator](); kernel.beginRun(DRIVER_TEST_IDS.runId); @@ -199,25 +205,20 @@ describe("AgentDriverKernelCore", () => { message: "x".repeat(1_024 * 1_024), retryable: false, }), - ).rejects.toThrow("queue reserve exceeds 1048576 UTF-8 JSON bytes"); - const retryError = { - code: "retry", - details: {}, - message: "retry fits", - retryable: false, - }; - const retry = kernel.failRun(retryError); - Reflect.set(retryError, "code", "mutated"); - Reflect.set(retryError, "message", "mutated after admission"); - await expect(retry).resolves.toBeUndefined(); - await kernel.completeRun(); + ).resolves.toBeUndefined(); + await expect(kernel.completeRun()).rejects.toThrow("conflicts"); await kernel.stop("test.complete"); await expect(events.next()).resolves.toMatchObject({ done: false, value: { kind: "run.failed", - payload: { error: { code: "retry" } }, + payload: { + error: { + code: "driver.error_oversized", + details: { originalBytes: expect.any(Number) }, + }, + }, runId: DRIVER_TEST_IDS.runId, }, }); @@ -304,7 +305,7 @@ describe("AgentDriverKernelCore", () => { ], }), ).rejects.toThrow("exceeds 1024 items"); - expect(kernel.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + expect(kernel.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toBeNull(); await kernel.failRun({ code: "terminal_admission_failed", @@ -342,10 +343,10 @@ describe("AgentDriverKernelCore", () => { payload: { stopReason: "end_turn" }, runId: DRIVER_TEST_IDS.runId, }; - kernel.beginRun(DRIVER_TEST_IDS.runId); + const ticket = kernel.beginRun(DRIVER_TEST_IDS.runId); await expect(kernel.pushEvents({ events: [completed, delta] })).rejects.toThrow( - "must be the final event", + "must be the only event", ); await expect( kernel.pushEvents({ @@ -365,12 +366,21 @@ describe("AgentDriverKernelCore", () => { await expect( kernel.pushEvents({ events: [{ ...completed, delivery: "best_effort" }] }), ).rejects.toThrow("must be lossless"); - expect(kernel.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + expect(kernel.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toBeNull(); - await expect(kernel.pushEvents({ events: [delta, completed] })).resolves.toMatchObject({ - accepted: [{ type: "message.delta" }, { type: "run.completed" }], + await expect(kernel.pushEvents({ events: [delta, completed] })).rejects.toThrow( + "must be the only event", + ); + await expect(kernel.pushEvents({ events: [delta] })).resolves.toMatchObject({ + accepted: [{ type: "message.delta" }], + }); + await expect(kernel.pushEvents({ events: [completed] })).resolves.toMatchObject({ + accepted: [{ type: "run.completed" }], + }); + expect(kernel.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toMatchObject({ + phase: "acked", + value: { status: "completed" }, }); - expect(kernel.runEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); await expect(kernel.pushEvents({ events: [delta] })).rejects.toThrow( "cannot target a terminated run", @@ -383,12 +393,15 @@ describe("AgentDriverKernelCore", () => { `Driver run ${DRIVER_TEST_IDS.runId} is already active.`, ); expect(kernel.currentRunId()).toBe(DRIVER_TEST_IDS.runId); - expect(kernel.runEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + expect(kernel.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toMatchObject({ + phase: "acked", + value: { status: "completed" }, + }); - kernel.endRun(DRIVER_TEST_IDS.runId); + kernel.releaseRun(ticket, "command_acked"); kernel.beginRun(DRIVER_TEST_IDS.secondRunId); expect(kernel.currentRunId()).toBe(DRIVER_TEST_IDS.secondRunId); - expect(kernel.runEventTerminal(DRIVER_TEST_IDS.secondRunId)).toBeNull(); + expect(kernel.runSnapshot(DRIVER_TEST_IDS.secondRunId)?.terminal).toBeNull(); }); test("treats stop before start as a terminal lifecycle", async () => { @@ -462,8 +475,9 @@ describe("AgentDriverKernelCore", () => { test("owns a command before the caller can mutate its identity or payload", async () => { const backend = createBackend(); let handledText: string | null = null; - backend.handleInput = async (_context, input) => { + backend.handleInput = async (context, input, runId, signal) => { handledText = input.text; + await settleBackendInput(context, runId, signal); }; const kernel = new AgentDriverKernelCore({ backendFactory: () => backend }); const command = { @@ -506,6 +520,7 @@ describe("AgentDriverKernelCore", () => { commandId: "owned-result-command", kind: "mcp.execute", requestId: "owned-result-request", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-server", toolCallId: "owned-result-tool", toolName: "tool", diff --git a/tests/agent-driver-kernel-commands.test.ts b/tests/agent-driver-kernel-commands.test.ts index 0baacfe..f28d99c 100644 --- a/tests/agent-driver-kernel-commands.test.ts +++ b/tests/agent-driver-kernel-commands.test.ts @@ -8,9 +8,18 @@ import type { import { AgentDriverKernelCore } from "../src/core/agent-driver-kernel"; import { DRIVER_EVENT_DELIVERY_TIMEOUT_MS } from "../src/core/driver-runtime-io"; import type { DriverEventInput } from "../src/protocol/events"; -import type { RuntimeCommand } from "../src/runtime-command"; +import { + RUNTIME_COMMAND_MAX_UTF8_BYTES, + measureRuntimeCommandJson, + type RuntimeCommand, +} from "../src/runtime-command"; import { settlePromiseWithTimeout } from "../src/utils/async"; -import { DRIVER_TEST_IDS, bootPayload, createBackend } from "./driver-runtime-boundary-fixtures"; +import { + DRIVER_TEST_IDS, + bootPayload, + createBackend, + settleBackendInput, +} from "./driver-runtime-boundary-fixtures"; describe("AgentDriverKernelCore", () => { test("does not publish diagnostics after an adapter run terminal", async () => { @@ -24,6 +33,10 @@ describe("AgentDriverKernelCore", () => { payload: { startedAt: new Date().toISOString() }, runId, }, + ], + }); + await context.ports.eventSink.pushEvents({ + events: [ { kind: "run.failed", payload: { @@ -131,33 +144,101 @@ describe("AgentDriverKernelCore", () => { }, }, }); - const text = "x".repeat(17 * 1_024 * 1_024); + const commandAtSize = (index: number) => { + const base = { + commandId: `large-command-${String(index)}`, + input: { text: "" }, + kind: "input.start" as const, + requestId: `large-request-${String(index)}`, + runId: DRIVER_TEST_IDS.runId, + }; + return { + ...base, + input: { text: "x".repeat(800 * 1_024 - measureRuntimeCommandJson(base)) }, + }; + }; await kernel.start(bootPayload); await pollEntered.promise; - const first = kernel.dispatch({ - commandId: "large-command-1", - input: { text }, - kind: "input.start", - requestId: "large-request-1", - runId: DRIVER_TEST_IDS.runId, - }); - void first.catch(() => {}); - await expect( - kernel.dispatch({ - commandId: "large-command-2", - input: { text }, - kind: "input.start", - requestId: "large-request-2", - runId: DRIVER_TEST_IDS.runId, - }), - ).rejects.toThrow("UTF-8 JSON bytes"); + const queued = Array.from({ length: 40 }, (_, index) => kernel.dispatch(commandAtSize(index))); + for (const pending of queued) void pending.catch(() => {}); + + expect(measureRuntimeCommandJson(commandAtSize(0))).toBe(800 * 1_024); + expect(800 * 1_024).toBeLessThan(RUNTIME_COMMAND_MAX_UTF8_BYTES); + await expect(kernel.dispatch(commandAtSize(40))).rejects.toThrow("UTF-8 JSON bytes"); (context as AgentDriverContext | null)?.lifecycle.fail(failure); - await expect(first).rejects.toBe(failure); + for (const pending of queued) await expect(pending).rejects.toBe(failure); await expect(kernel.stop("join failure")).rejects.toBe(failure); }); + test("rejects an oversized MCP command before external effects", async () => { + const calls: string[] = []; + const base = { + argumentsJson: "", + commandId: "oversized-mcp", + kind: "mcp.execute" as const, + requestId: "request-1", + runId: DRIVER_TEST_IDS.runId, + serverId: "server-1", + toolCallId: "tool-call-1", + toolName: "tool-1", + }; + const command = { + ...base, + argumentsJson: "x".repeat( + RUNTIME_COMMAND_MAX_UTF8_BYTES + 1 - measureRuntimeCommandJson(base), + ), + }; + const kernel = new AgentDriverKernelCore({ + backendFactory: () => createBackend(), + externalToolEffectLedger: { + async claimExternalToolEffect() { + calls.push("claim"); + throw new Error("unexpected claim"); + }, + async observeExternalToolEffect() { + calls.push("observe"); + throw new Error("unexpected observe"); + }, + async settleExternalToolEffect() { + calls.push("settle"); + throw new Error("unexpected settle"); + }, + }, + hostPorts: { + mcp: { + async prepare() { + calls.push("prepare"); + throw new Error("unexpected prepare"); + }, + }, + }, + }); + + expect(measureRuntimeCommandJson(command)).toBe(RUNTIME_COMMAND_MAX_UTF8_BYTES + 1); + await kernel.start(bootPayload); + await expect(kernel.dispatch(command)).rejects.toThrow("UTF-8 bytes"); + expect(calls).toEqual([]); + await expect(kernel.stop("test.stop")).resolves.toBeUndefined(); + }); + + test("publishes one unscoped completion when an idle driver stops", async () => { + const kernel = new AgentDriverKernelCore({ backendFactory: () => createBackend() }); + const events = kernel.events()[Symbol.asyncIterator](); + + await kernel.start(bootPayload); + await expect(kernel.stop("idle stop")).resolves.toBeUndefined(); + const terminal = await events.next(); + + expect(terminal).toMatchObject({ + done: false, + value: { kind: "run.completed" }, + }); + expect(terminal.value).not.toHaveProperty("runId"); + await expect(events.next()).resolves.toEqual({ done: true, value: undefined }); + }); + test.each([ ["poll", "resolve"], ["poll", "reject"], @@ -193,6 +274,7 @@ describe("AgentDriverKernelCore", () => { currentRunId: () => null, pushEvents: async ({ events }) => ({ accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), @@ -210,8 +292,8 @@ describe("AgentDriverKernelCore", () => { ? kernel .dispatch({ commandId: "blocked-accepted-command", - kind: "turn.cancel", - reason: "test.cancel", + kind: "session.stop", + reason: "test.stop", }) .catch(() => {}) : null; @@ -273,6 +355,7 @@ describe("AgentDriverKernelCore", () => { backend.handleInput = async () => { inputEntered.resolve(); await releaseInput.promise; + throw failure; }; backend.stop = async (_context, _reason, signal) => { stopEntered.resolve(); @@ -346,25 +429,23 @@ describe("AgentDriverKernelCore", () => { releaseInput.resolve(); await expect(input).rejects.toBe(failure); - await expect(observed).resolves.toEqual({ + await expect(observed).resolves.toMatchObject({ kind: "event", value: { done: false, value: { - kind: "run.failed", + kind: "diagnostic.reported", payload: { - error: { - code: "driver.runtime_failed", - details: {}, - message: failure.message, - retryable: false, - }, - recoverable: false, + code: "driver.command_failed", + message: failure.message, }, - runId: DRIVER_TEST_IDS.runId, }, }, }); + await expect(events.next()).resolves.toMatchObject({ + done: false, + value: { kind: "run.failed", runId: DRIVER_TEST_IDS.runId }, + }); await expect(events.next()).resolves.toEqual({ done: true, value: undefined }); await expect(kernel.stop("join backend failure")).rejects.toBe(failure); } finally { @@ -390,6 +471,7 @@ describe("AgentDriverKernelCore", () => { backend.handleInput = async () => { inputEntered.resolve(); await releaseInput.promise; + throw failure; }; backend.stop = async () => { cleanupEntered.resolve(); @@ -414,13 +496,17 @@ describe("AgentDriverKernelCore", () => { const stop = kernel.stop("wait for failure cleanup"); void stop.catch(() => {}); await cleanupEntered.promise; - const terminal = events.next(); - expect(await Promise.race([terminal.then(() => true), Bun.sleep(20).then(() => false)])).toBe( + const diagnostic = events.next(); + expect(await Promise.race([diagnostic.then(() => true), Bun.sleep(20).then(() => false)])).toBe( false, ); releaseCleanup.resolve(); await expect(stop).rejects.toBe(failure); - await expect(terminal).resolves.toMatchObject({ + await expect(diagnostic).resolves.toMatchObject({ + done: false, + value: { kind: "diagnostic.reported" }, + }); + await expect(events.next()).resolves.toMatchObject({ done: false, value: { kind: "run.failed", runId: DRIVER_TEST_IDS.runId }, }); @@ -438,6 +524,7 @@ describe("AgentDriverKernelCore", () => { backend.handleInput = async () => { inputEntered.resolve(); await releaseInput.promise; + throw failure; }; backend.stop = async () => { releaseInput.resolve(); @@ -459,6 +546,10 @@ describe("AgentDriverKernelCore", () => { await expect(input).rejects.toBe(failure); await expect(kernel.stop("wait for failed cleanup")).rejects.toBe(failure); + await expect(events.next()).resolves.toMatchObject({ + done: false, + value: { kind: "diagnostic.reported" }, + }); const terminal = events.next(); expect(await Promise.race([terminal.then(() => true), Bun.sleep(20).then(() => false)])).toBe( false, @@ -488,6 +579,7 @@ describe("AgentDriverKernelCore", () => { }); permissionEntered.resolve(); await permission; + throw failure; }; backend.stop = async () => { cleanupEntered.resolve(); @@ -559,14 +651,25 @@ describe("AgentDriverKernelCore", () => { done: false, value: { kind: "diagnostic.reported", + payload: { code: "permission.cancelled" }, runId: DRIVER_TEST_IDS.runId, }, }); await cleanupEntered.promise; + await expect(events.next()).resolves.toMatchObject({ + done: false, + value: { + kind: "diagnostic.reported", + payload: { code: "driver.command_failed" }, + }, + }); const terminal = events.next(); - expect(await Promise.race([terminal.then(() => true), Bun.sleep(20).then(() => false)])).toBe( - false, - ); + expect( + await Promise.race([ + terminal.then((value) => ({ kind: "event" as const, value })), + Bun.sleep(20).then(() => ({ kind: "pending" as const })), + ]), + ).toEqual({ kind: "pending" }); releaseCleanup.resolve(); await expect(terminal).resolves.toMatchObject({ done: false, @@ -586,9 +689,10 @@ describe("AgentDriverKernelCore", () => { const inputEntered = Promise.withResolvers(); const releaseInput = Promise.withResolvers(); let stopCount = 0; - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId, signal) => { inputEntered.resolve(); await releaseInput.promise; + await settleBackendInput(context, runId, signal); }; backend.stop = async () => { stopCount += 1; @@ -610,19 +714,6 @@ describe("AgentDriverKernelCore", () => { .catch(() => {}), ]; await inputEntered.promise; - pending.push( - kernel - .dispatch({ - commandId: "queued-input", - input: { text: "wait again" }, - kind: "input.start", - requestId: "queued-request", - runId: DRIVER_TEST_IDS.runId, - }) - .catch(() => {}), - ); - await Bun.sleep(0); - for (let index = 0; index < 1_024; index += 1) { pending.push( kernel @@ -630,6 +721,7 @@ describe("AgentDriverKernelCore", () => { commandId: `queued-cancel-${index}`, kind: "turn.cancel", reason: "queued", + runId: DRIVER_TEST_IDS.runId, }) .catch(() => {}), ); @@ -640,7 +732,7 @@ describe("AgentDriverKernelCore", () => { expect(stopCount).toBe(1); await expect(events.next()).resolves.toMatchObject({ done: false, - value: { kind: "run.completed" }, + value: { kind: "run.cancelled", runId: DRIVER_TEST_IDS.runId }, }); await expect(events.next()).resolves.toEqual({ done: true, value: undefined }); }); @@ -650,9 +742,10 @@ describe("AgentDriverKernelCore", () => { const inputEntered = Promise.withResolvers(); const releaseInput = Promise.withResolvers(); let stopCount = 0; - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId, signal) => { inputEntered.resolve(); await releaseInput.promise; + await settleBackendInput(context, runId, signal); }; backend.cancelActiveTurn = async (_context, reason) => { backend.cancelledReasons.push(reason); @@ -815,6 +908,7 @@ describe("AgentDriverKernelCore", () => { commandId: "after-stop-failure", kind: "turn.cancel", reason: "test", + runId: DRIVER_TEST_IDS.runId, }), ).rejects.toThrow("not accepting commands: failed"); }, @@ -883,23 +977,27 @@ describe("AgentDriverKernelCore", () => { const [skill] = await context.ports.skill.materialize(context.payload.execution, signal); materializedSkillName = skill?.skillName ?? null; }; - backend.handleInput = async (context: AgentDriverContext) => { + backend.handleInput = async (context: AgentDriverContext, _input, runId, signal) => { const command = { argumentsJson: '{"ok":true}', commandId: "mcp-port-1", kind: "mcp.execute" as const, requestId: "request-1", + runId: DRIVER_TEST_IDS.runId, serverId: "server-1", toolCallId: "tool-1", toolName: "complete", }; - const signal = new AbortController().signal; - await using prepared = await context.ports.mcp.prepare(command, signal); + const mcpSignal = new AbortController().signal; + await using prepared = await context.ports.mcp.prepare(command, mcpSignal); const result = await prepared.execute({ + attempt: 1, effectId: "effect-1", idempotencyKey: "effect-1", + kind: "claimed", }); mcpOutput = result.outputText; + await settleBackendInput(context, runId, signal); }; const kernel = new AgentDriverKernelCore({ backendFactory: () => backend, diff --git a/tests/agent-driver-kernel-lifecycle.test.ts b/tests/agent-driver-kernel-lifecycle.test.ts index d5d7c0a..9470e42 100644 --- a/tests/agent-driver-kernel-lifecycle.test.ts +++ b/tests/agent-driver-kernel-lifecycle.test.ts @@ -4,15 +4,68 @@ import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { AgentDriverKernelCore } from "../src/core/agent-driver-kernel"; import type { DriverEventInput } from "../src/protocol/events"; import type { RuntimeCommand } from "../src/runtime-command"; -import { DRIVER_TEST_IDS, bootPayload, createBackend } from "./driver-runtime-boundary-fixtures"; +import { + DRIVER_TEST_IDS, + bootPayload, + createBackend, + settleBackendInput, +} from "./driver-runtime-boundary-fixtures"; describe("AgentDriverKernelCore", () => { + test("commits a custom event sink terminal through the kernel lifecycle owner", async () => { + const backend = createBackend(); + const delivered: DriverEventInput[] = []; + backend.handleInput = async (context, _input, runId, signal) => { + expect(context.ports.eventSink.currentRunId()).toBe(runId); + await settleBackendInput(context, runId, signal); + }; + const kernel = new AgentDriverKernelCore({ + backendFactory: () => backend, + hostPorts: { + eventSink: { + commandUpdate: async () => {}, + currentRunId: () => DRIVER_TEST_IDS.secondRunId, + pushEvents: async ({ events }) => { + delivered.push(...structuredClone(events)); + return { + accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, + seq: index + 1, + type: event.kind, + })), + }; + }, + }, + }, + }); + + await kernel.start(bootPayload); + await expect( + kernel.dispatch({ + commandId: "custom-sink-input", + input: { text: "complete" }, + kind: "input.start", + requestId: "custom-sink-request", + runId: DRIVER_TEST_IDS.runId, + }), + ).resolves.toEqual({ requestId: "custom-sink-request" }); + + expect(delivered).toContainEqual( + expect.objectContaining({ kind: "run.completed", runId: DRIVER_TEST_IDS.runId }), + ); + expect(kernel.currentRunId()).toBeNull(); + await expect(kernel.stop("custom sink complete")).resolves.toBeUndefined(); + }); + test("refuses to synthesize an in-memory MCP effect ledger", async () => { const kernel = new AgentDriverKernelCore({ backendFactory: () => createBackend() }); await expect( kernel.claimExternalToolEffect( - { commandId: "mcp-command-without-durable-ledger" }, + { + claimToken: "00000000-0000-4000-8000-000000000001", + commandId: "mcp-command-without-durable-ledger", + }, new AbortController().signal, ), ).rejects.toThrow(/durable external tool effect ledger/); @@ -27,8 +80,17 @@ describe("AgentDriverKernelCore", () => { string, { outputText: string; requestId: string; serverId: string; toolName: string } >(); - backend.handleInput = async () => { + const activeRunEntered = Promise.withResolvers(); + const releaseActiveRun = Promise.withResolvers(); + backend.handleInput = async (context, _input, runId, signal) => { + if (kind === "mcp") { + activeRunEntered.resolve(); + await releaseActiveRun.promise; + await settleBackendInput(context, runId, signal); + return; + } calls += 1; + await settleBackendInput(context, runId, signal); }; const kernel = new AgentDriverKernelCore({ backendFactory: () => backend, @@ -38,13 +100,23 @@ describe("AgentDriverKernelCore", () => { const effectId = `test-effect-${commandId}`; return result === undefined - ? { attempt: 1, effectId, idempotencyKey: effectId, kind: "execute" as const } - : { effectId, kind: "completed" as const, result }; + ? { attempt: 1, effectId, idempotencyKey: effectId, kind: "claimed" as const } + : { effectId, kind: "succeeded" as const, result }; }, - completeExternalToolEffect: async ({ commandId, result }) => { - effectResults.set(commandId, structuredClone(result)); + observeExternalToolEffect: async ({ commandId }) => { + const result = effectResults.get(commandId); + const effectId = `test-effect-${commandId}`; + return result === undefined + ? { effectId, kind: "intent" as const } + : { effectId, kind: "succeeded" as const, result }; + }, + settleExternalToolEffect: async ({ commandId, effectId, settlement }) => { + if (settlement.kind === "unknown") { + return { effectId, kind: "unknown" as const }; + } + effectResults.set(commandId, structuredClone(settlement.result)); + return { effectId, kind: "succeeded" as const, result: settlement.result }; }, - markExternalToolEffectUnknown: async () => {}, }, hostPorts: { mcp: { @@ -52,7 +124,6 @@ describe("AgentDriverKernelCore", () => { async execute() { calls += 1; return { - debug: { nested: "original" }, outputText: `ran ${command.toolName}`, requestId: command.requestId, serverId: command.serverId, @@ -78,28 +149,39 @@ describe("AgentDriverKernelCore", () => { commandId: "replayed-command", kind: "mcp.execute", requestId: "request-replay", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: "tool-replay", toolName: "createIssue", }; await kernel.start(bootPayload); + const activeRun = + kind === "mcp" + ? kernel.dispatch({ + commandId: "active-run-command", + input: { text: "keep the run active" }, + kind: "input.start", + requestId: "active-run-request", + runId: DRIVER_TEST_IDS.runId, + }) + : null; + if (activeRun !== null) { + await activeRunEntered.promise; + } const first = await kernel.dispatch(command); if (typeof first === "object" && first !== null) { Reflect.set(first, "requestId", "caller-mutated"); - const debug = Reflect.get(first, "debug") as { nested?: string } | undefined; - if (debug !== undefined) { - debug.nested = "caller-mutated"; - } } const replay = await kernel.dispatch(structuredClone(command)); + releaseActiveRun.resolve(); + await activeRun; await kernel.stop("test.stop"); expect(replay).toEqual( kind === "input" ? { requestId: "request-replay" } : { - debug: { nested: "original" }, outputText: "ran createIssue", requestId: "request-replay", serverId: "mcp-linear", @@ -119,8 +201,9 @@ describe("AgentDriverKernelCore", () => { role: "agent", }, }; - backend.handleInput = async (context: AgentDriverContext) => { + backend.handleInput = async (context: AgentDriverContext, _input, runId, signal) => { await context.ports.eventSink.pushEvents({ events: [event] }); + await settleBackendInput(context, runId, signal); }; const kernel = new AgentDriverKernelCore({ backendFactory: () => backend, @@ -138,7 +221,7 @@ describe("AgentDriverKernelCore", () => { runId: DRIVER_TEST_IDS.runId, }); - await expect(events.next()).resolves.toEqual({ + await expect(events.next()).resolves.toMatchObject({ done: false, value: event, }); @@ -150,12 +233,32 @@ describe("AgentDriverKernelCore", () => { test("turn cancel dispatches through the active backend", async () => { const backend = createBackend(); + const inputEntered = Promise.withResolvers(); + const releaseInput = Promise.withResolvers(); + backend.handleInput = async (context, _input, runId, signal) => { + inputEntered.resolve(); + await releaseInput.promise; + await settleBackendInput(context, runId, signal); + }; + backend.cancelActiveTurn = async (_context, reason) => { + backend.cancelledReasons.push(reason); + releaseInput.resolve(); + }; const kernel = new AgentDriverKernelCore({ backendFactory: () => backend, }); await kernel.start(bootPayload); + const input = kernel.dispatch({ + commandId: "cancel-active-input", + input: { text: "wait" }, + kind: "input.start", + requestId: "cancel-active-request", + runId: DRIVER_TEST_IDS.runId, + }); + await inputEntered.promise; await kernel.cancel("test.cancel"); + await input; expect(backend.cancelledReasons).toEqual(["test.cancel"]); await kernel.stop("test.stop"); @@ -167,9 +270,10 @@ describe("AgentDriverKernelCore", () => { const inputEntered = Promise.withResolvers(); const releaseInput = Promise.withResolvers(); const cancelEntered = Promise.withResolvers(); - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId, signal) => { inputEntered.resolve(); await releaseInput.promise; + await settleBackendInput(context, runId, signal); }; backend.cancelActiveTurn = async (_context, reason) => { backend.cancelledReasons.push(reason); @@ -198,7 +302,7 @@ describe("AgentDriverKernelCore", () => { await kernel.stop("test.stop"); }); - test("treats an input error after the local abort as cancellation", async () => { + test("keeps an ordinary input error failed after the local abort", async () => { const backend = createBackend(); const inputEntered = Promise.withResolvers(); const cancelInput = Promise.withResolvers(); @@ -221,10 +325,9 @@ describe("AgentDriverKernelCore", () => { }); await inputEntered.promise; - await expect(Promise.all([input, kernel.cancel("test.cancel")])).resolves.toEqual([ - undefined, - undefined, - ]); + await expect(Promise.all([input, kernel.cancel("test.cancel")])).rejects.toThrow( + "native cancellation", + ); await expect(kernel.stop("test.stop")).resolves.toBeUndefined(); }); @@ -241,12 +344,13 @@ describe("AgentDriverKernelCore", () => { let inputCount = 0; let permission: Promise | null = null; let resolutionRunId: unknown; - backend.handleInput = async (context) => { + backend.handleInput = async (context, _input, runId, signal) => { inputCount += 1; if (inputCount > 1) { nextInputEntered.resolve(); await releaseNextInput.promise; + await settleBackendInput(context, runId, signal); return; } @@ -259,6 +363,7 @@ describe("AgentDriverKernelCore", () => { }); inputEntered.resolve(); await releaseInput.promise; + await settleBackendInput(context, runId, signal); }; backend.cancelActiveTurn = async () => { releaseInput.resolve(); @@ -292,17 +397,22 @@ describe("AgentDriverKernelCore", () => { runId: DRIVER_TEST_IDS.runId, }); await inputEntered.promise; - await expect(Promise.all([firstInput, kernel.cancel("test.cancel")])).resolves.toEqual([ + const cancellation = kernel.cancel("test.cancel"); + await resolutionPublishing.promise; + releaseResolution.resolve(); + await expect(Promise.all([firstInput, cancellation])).resolves.toEqual([ undefined, undefined, ]); - await kernel.dispatch({ - commandId: "resolve-stale-permission", - decision, - kind: "permission.resolve", - requestId: "permission-stale", - }); - await resolutionPublishing.promise; + await expect( + kernel.dispatch({ + commandId: "resolve-stale-permission", + decision, + kind: "permission.resolve", + requestId: "permission-stale", + runId: DRIVER_TEST_IDS.runId, + }), + ).rejects.toThrow("does not target the active run"); const nextInput = kernel.dispatch({ commandId: "next-input", input: { text: "continue" }, @@ -311,7 +421,6 @@ describe("AgentDriverKernelCore", () => { runId: DRIVER_TEST_IDS.secondRunId, }); await nextInputEntered.promise; - releaseResolution.resolve(); await expect(permission).resolves.toBe("reject_once"); expect(resolutionRunId).toBe(DRIVER_TEST_IDS.runId); releaseNextInput.resolve(); @@ -364,8 +473,10 @@ describe("AgentDriverKernelCore", () => { requestId: "permission-delivery-request", runId: DRIVER_TEST_IDS.runId, }); + void input.catch(() => {}); await inputEntered.promise; const cancel = kernel.cancel("test.cancel"); + void cancel.catch(() => {}); await deliveryEntered.promise; releaseDelivery.resolve(); @@ -380,7 +491,7 @@ describe("AgentDriverKernelCore", () => { runId: DRIVER_TEST_IDS.secondRunId, }), ).rejects.toThrow("not accepting commands: failed"); - await kernel.stop("test.stop"); + await expect(kernel.stop("test.stop")).rejects.toThrow("could not be delivered"); }, ); @@ -429,6 +540,7 @@ describe("AgentDriverKernelCore", () => { commandId: "after-start-failure", kind: "turn.cancel", reason: "test", + runId: DRIVER_TEST_IDS.runId, }), ).rejects.toThrow("not accepting commands: failed"); expect(stopCount).toBe(expectedStopCount); diff --git a/tests/async.test.ts b/tests/async.test.ts index e8b0598..af31494 100644 --- a/tests/async.test.ts +++ b/tests/async.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test"; import { promiseWithTimeout, raceWithAbort, + readBoundedStreamBytes, settlePromiseWithTimeout, sleepPromise, } from "../src/utils/async"; @@ -98,6 +99,29 @@ describe("async lifecycle utilities", () => { ); }); + test("does not wait for an unresponsive stream cancellation", async () => { + const controller = new AbortController(); + const reason = new Error("stream cancelled"); + let cancelled = false; + const body = new ReadableStream({ + cancel() { + cancelled = true; + return new Promise(() => {}); + }, + pull() {}, + }); + const read = readBoundedStreamBytes(body, 1, new Error("too large"), controller.signal); + + controller.abort(reason); + + const result = await settlePromiseWithTimeout(read, { + label: "bounded stream cancellation", + timeoutMs: 100, + }); + expect(result).toEqual({ error: reason, status: "failed" }); + expect(cancelled).toBe(true); + }); + test.each([ ["NaN", Number.NaN], ["positive infinity", Number.POSITIVE_INFINITY], diff --git a/tests/claude-agent-sdk-driver-backend.test.ts b/tests/claude-agent-sdk-driver-backend.test.ts index 0261256..e884fef 100644 --- a/tests/claude-agent-sdk-driver-backend.test.ts +++ b/tests/claude-agent-sdk-driver-backend.test.ts @@ -145,7 +145,11 @@ function createHarness( } } return { - accepted: batch.map((event) => ({ seq: (seq += 1), type: event.kind })), + accepted: batch.map((event) => ({ + eventId: event.sourceEventId!, + seq: (seq += 1), + type: event.kind, + })), }; }, }, @@ -154,9 +158,22 @@ function createHarness( permission: { request: async () => "allow_once" }, ports: { skill: { materialize: async () => [] } }, }); + const backend = new ClaudeAgentSdkDriverBackend(payload, dependencies); + const handleInput = backend.handleInput.bind(backend); + backend.handleInput = async (inputContext, input, runId) => { + currentRunId ??= runId; + + try { + await handleInput(inputContext, input, runId); + } finally { + if (currentRunId === runId) { + currentRunId = null; + } + } + }; return { - backend: new ClaudeAgentSdkDriverBackend(payload, dependencies), + backend, context, events, }; diff --git a/tests/claude-agent-sdk-durability.test.ts b/tests/claude-agent-sdk-durability.test.ts index 025be2a..0a43383 100644 --- a/tests/claude-agent-sdk-durability.test.ts +++ b/tests/claude-agent-sdk-durability.test.ts @@ -335,7 +335,7 @@ describe("Claude Agent SDK durable event boundaries", () => { } return { accepted: batch.map((event) => ({ - eventId: event.sourceEventId, + eventId: event.sourceEventId!, seq: (sequence += 1), type: event.kind, })), diff --git a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts index 4ccd961..8df26ab 100644 --- a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts @@ -908,10 +908,10 @@ describe("Claude Agent SDK provider fixtures", () => { return []; } - const rawInput = event.payload["rawInput"]; + const rawInputDelta = event.payload["rawInputDelta"]; const toolCallId = event.payload["toolCallId"]; - return typeof rawInput === "string" && typeof toolCallId === "string" - ? [{ rawInput, toolCallId }] + return typeof rawInputDelta === "string" && typeof toolCallId === "string" + ? [{ rawInputDelta, toolCallId }] : []; }); const thoughtDeltas = events().flatMap((event) => { @@ -930,9 +930,9 @@ describe("Claude Agent SDK provider fixtures", () => { ); expect(toolArguments).toEqual([ - { rawInput: '{"a":', toolCallId: "tool-a" }, - { rawInput: '{"b":1}', toolCallId: "tool-b" }, - { rawInput: "1}", toolCallId: "tool-a" }, + { rawInputDelta: '{"a":', toolCallId: "tool-a" }, + { rawInputDelta: '{"b":1}', toolCallId: "tool-b" }, + { rawInputDelta: "1}", toolCallId: "tool-a" }, ]); expect(thoughts["A1"]).toBe(thoughts["A2"]); expect(thoughts["A1"]).not.toBe(thoughts["B1"]); diff --git a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts index 595863d..8c6207d 100644 --- a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts @@ -191,6 +191,43 @@ describe("Claude Agent SDK provider fixtures", () => { }); }); + test("clears the prior task snapshot when the visible-task bound is exceeded", async () => { + const { context, events, translator } = createHarness(); + const message = (tasks: Array<{ description: string; task_id: string; task_type: string }>) => + ({ + session_id: "native-session-1", + subtype: "background_tasks_changed", + tasks, + type: "system", + uuid: `background-tasks-${String(tasks.length)}`, + }) as unknown as SDKMessage; + + await translator.handleSdkMessage( + context, + message([{ description: "Inspect", task_id: "task-1", task_type: "local_agent" }]), + "run-1" as RunId, + ); + await translator.handleSdkMessage( + context, + message( + Array.from({ length: 257 }, (_, index) => ({ + description: "Inspect", + task_id: `task-${String(index)}`, + task_type: "local_agent", + })), + ), + "run-1" as RunId, + ); + + expect(events().filter((event) => event.kind === "agent.tasks.replaced")).toMatchObject([ + { payload: { tasks: [{ taskId: "task-1" }] } }, + { payload: { tasks: [] } }, + ]); + expect(events().filter((event) => event.kind === "diagnostic.reported")).toMatchObject([ + { payload: { code: "claude.visible_background_tasks_too_many" } }, + ]); + }); + test("projects informational, local command, mirror failure, and conversation reset frames", async () => { const { context, events, nativeSessionResets, translator } = createHarness(); const messages = [ @@ -939,6 +976,24 @@ describe("Claude Agent SDK provider fixtures", () => { await translator.handleSdkMessage(context, message, "run-1" as RunId); } + expect( + events().filter( + (event) => + event.kind === "tool.call.updated" && + event.delivery === "best_effort" && + isRecord(event.payload) && + event.payload["toolCallId"] === "tool-1", + ), + ).toContainEqual( + expect.objectContaining({ + payload: expect.objectContaining({ rawInputDelta: '{"path":"partial' }), + }), + ); + expect(events()).not.toContainEqual( + expect.objectContaining({ + payload: expect.objectContaining({ rawInput: "{}", toolCallId: "tool-1" }), + }), + ); expect( events().filter( (event) => diff --git a/tests/claude-agent-sdk-task-events.test.ts b/tests/claude-agent-sdk-task-events.test.ts index b4f4d00..bd1b880 100644 --- a/tests/claude-agent-sdk-task-events.test.ts +++ b/tests/claude-agent-sdk-task-events.test.ts @@ -95,9 +95,6 @@ describe("Claude Agent SDK task snapshots", () => { const maximum = snapshot(Array.from({ length: 256 }, (_, index) => task(`task-${index}`))); expect(maximum).toMatchObject({ kind: "agent.tasks.replaced" }); - if (maximum === null) { - throw new Error("Expected a task replacement snapshot."); - } expect((maximum.payload as { tasks: unknown[] }).tasks).toHaveLength(256); expect( @@ -108,16 +105,37 @@ describe("Claude Agent SDK task snapshots", () => { payload: { code: "claude.visible_background_tasks_too_many" }, visibility: "owner_debug", }, - snapshot: null, + snapshot: claudeBackgroundTasksClosedEvent(), + }); + const unreadTail = task("unread-tail"); + Object.defineProperty(unreadTail, "ambient", { + get() { + throw new Error("Tasks after the visible limit must not be inspected."); + }, + }); + expect( + projection([ + ...Array.from({ length: 257 }, (_, index) => task(`bounded-${index}`)), + unreadTail, + ]), + ).toMatchObject({ + diagnostic: { payload: { code: "claude.visible_background_tasks_too_many" } }, + snapshot: claudeBackgroundTasksClosedEvent(), }); expect( projection(Array.from({ length: 1_025 }, (_, index) => task(`ambient-${index}`, "x", true))), ).toMatchObject({ - diagnostic: { - kind: "diagnostic.reported", - payload: { code: "claude.background_tasks_snapshot_too_large" }, - }, - snapshot: null, + diagnostic: { payload: { code: "claude.background_tasks_snapshot_too_large" } }, + snapshot: claudeBackgroundTasksClosedEvent(), + }); + expect( + projection([ + ...Array.from({ length: 1_023 }, (_, index) => task(`ambient-${index}`, "x", true)), + task("visible"), + ]), + ).toMatchObject({ snapshot: { payload: { tasks: [{ taskId: "visible" }] } } }); + expect(projection(Array.from({ length: 1_024 }, () => task("duplicate")))).toMatchObject({ + snapshot: { payload: { tasks: [{ taskId: "duplicate" }] } }, }); const oversizedMetadata = projection( Array.from({ length: 100 }, (_, index) => ({ @@ -137,9 +155,6 @@ describe("Claude Agent SDK task snapshots", () => { visibility: "participant", }, }); - if (oversizedMetadata.snapshot === null) { - throw new Error("Expected an ID-only membership snapshot."); - } expect( (oversizedMetadata.snapshot.payload as { tasks: Array> }).tasks, ).toEqual(Array.from({ length: 100 }, (_, index) => ({ taskId: `large-${index}` }))); diff --git a/tests/cma-http-dispatch.test.ts b/tests/cma-http-dispatch.test.ts index 3c1c3dc..54b06fa 100644 --- a/tests/cma-http-dispatch.test.ts +++ b/tests/cma-http-dispatch.test.ts @@ -68,7 +68,7 @@ function messageEvent( occurredAt: "2026-01-01T00:00:01.000Z", origin: "driver", payload, - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId, visibility: "participant", }); diff --git a/tests/cma-http-request.test.ts b/tests/cma-http-request.test.ts index 79f5294..ef69744 100644 --- a/tests/cma-http-request.test.ts +++ b/tests/cma-http-request.test.ts @@ -8,6 +8,7 @@ import { CMA_DEFAULT_BETA_HEADER_VALUE, createCmaHttpHandler, } from "../src/surfaces/cma-http"; +import { readCmaJsonBody } from "../src/surfaces/cma-http/request"; function cmaRequest(path: string, init: RequestInit = {}): Request { const headers = new Headers(init.headers); @@ -180,6 +181,21 @@ describe("CMA HTTP surface", () => { expect(canceled).toBe(true); }); + test("propagates request cancellation while reading a streaming body", async () => { + const controller = new AbortController(); + const reason = new Error("client disconnected"); + const request = new Request("https://driver.test/v1/agents", { + body: new ReadableStream({ pull() {} }), + method: "POST", + signal: controller.signal, + }); + const pending = readCmaJsonBody(request); + + controller.abort(reason); + + await expect(pending).rejects.toBe(reason); + }); + test("rejects an inbound event whose admitted record exceeds the wire limit", async () => { let dispatches = 0; const store = createCmaMemoryStore({ sessions: [{ id: "session-1" }] }); diff --git a/tests/cma-http-sse.test.ts b/tests/cma-http-sse.test.ts index 531a076..f002649 100644 --- a/tests/cma-http-sse.test.ts +++ b/tests/cma-http-sse.test.ts @@ -53,7 +53,7 @@ function runFailedEvent(sessionId: string, recoverable = false) { recoverable, }, runId: createDriverId(), - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId, visibility: "participant", }); diff --git a/tests/cma-projection.test.ts b/tests/cma-projection.test.ts index 5922c7a..30b81dd 100644 --- a/tests/cma-projection.test.ts +++ b/tests/cma-projection.test.ts @@ -24,13 +24,17 @@ describe("CMA projection", () => { kind: "message.added", payload: { content: [{ text: "message", type: "text" }], + messageId: "message-1", preventContinuation: "yes", }, }, - { kind: "message.added", payload: { content: "message", phase: "final_answer" } }, { kind: "message.added", - payload: { content: "message", memoryCitation: Symbol("bad") }, + payload: { content: "message", messageId: "message-1", phase: "final_answer" }, + }, + { + kind: "message.added", + payload: { content: "message", memoryCitation: Symbol("bad"), messageId: "message-1" }, }, { kind: "tool.call.updated", @@ -57,11 +61,13 @@ describe("CMA projection", () => { }; const result = ingestRuntimeEventInput(context, { kind: "message.added", - payload: { content: "answer", memoryCitation, phase: "final" }, + payload: { content: "answer", memoryCitation, messageId: "message-1", phase: "final" }, }); expect(result).toMatchObject({ - event: { payload: { content: "answer", memoryCitation, phase: "final" } }, + event: { + payload: { content: "answer", memoryCitation, messageId: "message-1", phase: "final" }, + }, status: "accepted", }); }); diff --git a/tests/cma-sdk.test.ts b/tests/cma-sdk.test.ts index 226ce85..a610eb9 100644 --- a/tests/cma-sdk.test.ts +++ b/tests/cma-sdk.test.ts @@ -160,7 +160,7 @@ describe("CMA SDK client", () => { content: "hello", messageId: "message-1", }, - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId, visibility: "participant", }), @@ -205,7 +205,7 @@ describe("CMA SDK client", () => { occurredAt: "2026-01-01T00:00:01.000Z", origin: "driver", payload: { messageId: "message-1" }, - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId, visibility: "participant", }), @@ -220,7 +220,7 @@ describe("CMA SDK client", () => { occurredAt: "2026-01-01T00:00:02.000Z", origin: "driver", payload: { messageId: "message-2" }, - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId, visibility: "participant", }), @@ -360,7 +360,7 @@ describe("CMA SDK client", () => { occurredAt: "2026-01-01T00:00:01.000Z", origin: "driver", payload: { content, messageId: "message-1" }, - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId, visibility: "participant", }); diff --git a/tests/cma-store-claims.test.ts b/tests/cma-store-claims.test.ts index a427399..454de24 100644 --- a/tests/cma-store-claims.test.ts +++ b/tests/cma-store-claims.test.ts @@ -1,9 +1,8 @@ import { describe, expect, test } from "bun:test"; -import type { CmaInboundEvent } from "../src/projections/cma"; +import type { CmaInboundEvent, CmaProjectedDriverCommand } from "../src/projections/cma"; import { createDriverId } from "../src/protocol/id"; import { parseRuntimeEventEnvelope } from "../src/runtime-events"; -import type { RuntimeCommand } from "../src/runtime-command"; import { CmaStoreConflictError } from "../src/stores/cma-store"; import { createCmaMemoryStore } from "../src/stores/memory"; @@ -33,7 +32,7 @@ function driverEvent( origin: "driver", payload, ...(options.runId === undefined ? {} : { runId: options.runId }), - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId, ...(options.sourceEventId === undefined ? {} : { sourceEventId: options.sourceEventId }), visibility: options.visibility ?? "participant", @@ -44,7 +43,7 @@ function interrupt( commandId: string, reason?: string, ): { - readonly command: RuntimeCommand; + readonly command: CmaProjectedDriverCommand; readonly event: CmaInboundEvent; } { return { diff --git a/tests/cma-store-events.test.ts b/tests/cma-store-events.test.ts index f2ba440..5c77608 100644 --- a/tests/cma-store-events.test.ts +++ b/tests/cma-store-events.test.ts @@ -36,7 +36,7 @@ function driverEvent( origin: "driver", payload, ...(options.runId === undefined ? {} : { runId: options.runId }), - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId, ...(options.sourceEventId === undefined ? {} : { sourceEventId: options.sourceEventId }), visibility: options.visibility ?? "participant", @@ -50,13 +50,13 @@ describe("CMA memory store lifecycle", () => { const sourceEventId = createDriverId(); const store = createCmaMemoryStore({ sessions: [{ id: sessionId }] }); const payload = JSON.parse( - '{"__proto__":{"recoverable":true},"error":{"code":"fatal","details":{},"message":"failed","retryable":false}}', + '{"__proto__":{"recoverable":true},"error":{"code":"fatal","details":{},"message":"failed","retryable":false},"recoverable":false}', ); const event = driverEvent(sessionId, "run.failed", payload, { runId, sourceEventId }); const eventPayload = event.payload as Record; expect(Object.hasOwn(eventPayload, "__proto__")).toBe(true); - expect(eventPayload["recoverable"]).toBeUndefined(); + expect(eventPayload["recoverable"]).toBe(false); const first = await store.appendDriverEvent(sessionId, event); expect(first).toMatchObject([ @@ -140,7 +140,10 @@ describe("CMA memory store lifecycle", () => { ), ).rejects.toBeInstanceOf(CmaStoreConflictError); await expect( - store.appendDriverEvent(sessionId, driverEvent(otherSessionId, "message.completed", {})), + store.appendDriverEvent( + sessionId, + driverEvent(otherSessionId, "message.completed", { messageId: "message-1" }), + ), ).rejects.toThrow("sessionId"); expect(await store.listSessionEvents(otherSessionId)).toHaveLength(0); }); diff --git a/tests/cma-store-stream.test.ts b/tests/cma-store-stream.test.ts index 7d97327..b2a382e 100644 --- a/tests/cma-store-stream.test.ts +++ b/tests/cma-store-stream.test.ts @@ -1,9 +1,8 @@ import { describe, expect, test } from "bun:test"; -import type { CmaInboundEvent } from "../src/projections/cma"; +import type { CmaInboundEvent, CmaProjectedDriverCommand } from "../src/projections/cma"; import { createDriverId } from "../src/protocol/id"; import { parseRuntimeEventEnvelope } from "../src/runtime-events"; -import type { RuntimeCommand } from "../src/runtime-command"; import { CMA_MAX_EVENT_BYTES, CMA_MAX_REPLAY_BYTES, @@ -37,7 +36,7 @@ function driverEvent( origin: "driver", payload, ...(options.runId === undefined ? {} : { runId: options.runId }), - schemaVersion: "2026-05-26", + schemaVersion: "2026-08-29", sessionId, ...(options.sourceEventId === undefined ? {} : { sourceEventId: options.sourceEventId }), visibility: options.visibility ?? "participant", @@ -48,7 +47,7 @@ function interrupt( commandId: string, reason?: string, ): { - readonly command: RuntimeCommand; + readonly command: CmaProjectedDriverCommand; readonly event: CmaInboundEvent; } { return { diff --git a/tests/driver-artifact-live.test.ts b/tests/driver-artifact-live.test.ts index 98ee13e..a2ba906 100644 --- a/tests/driver-artifact-live.test.ts +++ b/tests/driver-artifact-live.test.ts @@ -6,6 +6,7 @@ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises" import { tmpdir } from "node:os"; import { isAbsolute, join, resolve } from "node:path"; +import { DRIVER_PROTOCOL_VERSION } from "../src/protocol/boot"; import { DriverArtifactTestController, expectedDriverCapabilities, @@ -893,7 +894,7 @@ function createBootPayload(input: { skills: [], }, heartbeatIntervalMs: 60_000, - protocolVersion: 2, + protocolVersion: DRIVER_PROTOCOL_VERSION, runtime: runtimeCase.runtime, runtimeTransport: runtimeCase.transport, sandboxId, @@ -2177,6 +2178,7 @@ async function testCancellation(runtimeCase: LiveRuntimeCase): Promise { commandId: startBoundaryCancelId, kind: "turn.cancel", reason: "live.cancel.start_boundary", + runId: startBoundary.runId, }); await controller.waitForCommandUpdate( (update) => update.commandId === startBoundaryCancelId && update.status === "accepted", @@ -2256,7 +2258,12 @@ async function testCancellation(runtimeCase: LiveRuntimeCase): Promise { } }); const cancelId = `cancel-${createTestId()}`; - controller.enqueue({ commandId: cancelId, kind: "turn.cancel", reason: "live.cancel" }); + controller.enqueue({ + commandId: cancelId, + kind: "turn.cancel", + reason: "live.cancel", + runId: active.runId, + }); const [inputUpdate, cancelUpdate, cancelledEvent] = await Promise.all([ controller.waitForCommandTerminal(active.commandId, LIVE_TURN_TIMEOUT_MS), controller.waitForCommandTerminal(cancelId, LIVE_TURN_TIMEOUT_MS), @@ -2289,7 +2296,12 @@ async function testCancellation(runtimeCase: LiveRuntimeCase): Promise { "run.cancelled", ); const replayIndex = controller.commandUpdates.length; - controller.enqueue({ commandId: cancelId, kind: "turn.cancel", reason: "live.cancel" }); + controller.enqueue({ + commandId: cancelId, + kind: "turn.cancel", + reason: "live.cancel", + runId: active.runId, + }); expect( (await controller.waitForCommandTerminal(cancelId, LIVE_STOP_TIMEOUT_MS, replayIndex)) .status, @@ -2309,10 +2321,11 @@ async function testCancellation(runtimeCase: LiveRuntimeCase): Promise { commandId: idleCancelId, kind: "turn.cancel", reason: "live.idle.cancel", + runId: active.runId, }); expect( (await controller.waitForCommandTerminal(idleCancelId, LIVE_STOP_TIMEOUT_MS)).status, - ).toBe("completed"); + ).toBe("failed"); const events = await runTurn( controller, @@ -2426,6 +2439,7 @@ async function testSupervisedPermission(runtimeCase: LiveRuntimeCase): Promise @@ -2507,6 +2522,7 @@ async function testSupervisedPermission(runtimeCase: LiveRuntimeCase): Promise process.stdout.write(JSON.stringify(message) + "\n"); const mcpRequest = async (server, message) => { const response = await fetch(server.url, { @@ -43,14 +46,22 @@ const mcpRequest = async (server, message) => { return message.id === undefined ? undefined : response.json(); }; const handle = async (message) => { - if (!("method" in message) || !("id" in message)) return; + if (!("method" in message)) return; + if (message.method === "session/cancel") { + if (pendingPromptId !== undefined) { + send({ id: pendingPromptId, jsonrpc: "2.0", result: { stopReason: "cancelled" } }); + pendingPromptId = undefined; + } + return; + } + if (!("id" in message)) return; let result; switch (message.method) { case "initialize": result = { agentCapabilities: { mcpCapabilities: { http: true }, - sessionCapabilities: { close: {} }, + sessionCapabilities: { close: {}, resume: {} }, }, authMethods: [], protocolVersion: 1, @@ -85,8 +96,12 @@ const handle = async (message) => { break; } case "session/close": + case "session/resume": result = {}; break; + case "session/prompt": + pendingPromptId = message.id; + return; default: result = {}; } @@ -114,15 +129,18 @@ function mcpCommand( commandId: string, toolName: string, argumentsJson = "{}", + runId: string = driverBootPayload.execution.configRevision.runId, ): DriverArtifactTestCommand { return { argumentsJson, commandId, kind: "mcp.execute", requestId: `request-${commandId}`, + runId, serverId: MCP_SERVER_ID, + toolCallId: `tool-call-${commandId}`, toolName, - }; + } satisfies McpExecuteCommand; } function jsonResponse(id: unknown, result: unknown, headers?: HeadersInit): Response { @@ -158,11 +176,11 @@ artifactTest( const sessions = new Map(); const toolCalls = new Map(); let deleteRequests = 0; - let hangingRequestId: unknown; + let cancellationNotifications = 0; let invalidSessionHeaders = 0; let unauthorizedRequests = 0; const hangStarted = Promise.withResolvers(); - const hangCancelled = Promise.withResolvers(); + const hangReleased = Promise.withResolvers(); const server = Bun.serve({ hostname: "127.0.0.1", port: 0, @@ -218,9 +236,7 @@ artifactTest( return new Response("Invalid MCP session headers.", { status: 400 }); } if (message.method === "notifications/cancelled") { - if (hangingRequestId !== undefined && message.params?.requestId === hangingRequestId) { - hangCancelled.resolve(); - } + cancellationNotifications += 1; return new Response(null, { status: 202 }); } if (message.method === "notifications/initialized" || message.id === undefined) { @@ -310,12 +326,10 @@ artifactTest( }); } if (toolName === "hang") { - hangingRequestId = message.id; hangStarted.resolve(); - await hangCancelled.promise; + await hangReleased.promise; return jsonResponse(message.id, { - content: [{ text: "cancelled", type: "text" }], - isError: true, + content: [{ text: "committed after cancellation", type: "text" }], }); } if (toolName === "tool-error") { @@ -400,6 +414,23 @@ artifactTest( ]); expect(unauthorizedRequests).toBe(0); + const initialRunEventIndex = controller.events.length; + controller.enqueue({ + commandId: "input-mcp-run", + input: { text: "hold MCP run open" }, + kind: "input.start", + requestId: "request-input-mcp-run", + runId: driverBootPayload.execution.configRevision.runId, + }); + await controller.waitForEvent( + (event) => + event.kind === "run.started" && + event.runId === driverBootPayload.execution.configRevision.runId, + initialRunEventIndex, + 10_000, + "initial MCP run start", + ); + const counterCommand = mcpCommand("mcp-counter", "counter"); controller.enqueue(counterCommand); const counter = await controller.waitForCommandTerminal("mcp-counter", 10_000); @@ -469,16 +500,41 @@ artifactTest( commandId: "cancel-mcp-hang", kind: "turn.cancel", reason: "artifact.mcp.test.cancel", + runId: driverBootPayload.execution.configRevision.runId, }); - const [hangUpdate, cancelUpdate] = await Promise.all([ + hangReleased.resolve(); + const [hangUpdate, cancelUpdate, initialRunUpdate] = await Promise.all([ controller.waitForCommandTerminal("mcp-hang", 10_000), controller.waitForCommandTerminal("cancel-mcp-hang", 10_000), + controller.waitForCommandTerminal("input-mcp-run", 10_000), ]); - expect(hangUpdate.status).toBe("cancelled"); + controller.assertHealthy("committed MCP cancellation"); + expect(hangUpdate).toMatchObject({ + result: { outputText: "committed after cancellation" }, + status: "completed", + }); expect(cancelUpdate.status).toBe("completed"); - await withTimeout(hangCancelled.promise, "MCP cancellation notification"); + expect(initialRunUpdate.status).toBe("cancelled"); + expect(cancellationNotifications).toBe(0); - controller.enqueue(mcpCommand("mcp-after-cancel", "echo", '{"value":"recovered"}')); + const recoveryRunEventIndex = controller.events.length; + controller.enqueue({ + commandId: "input-mcp-recovery-run", + input: { text: "hold recovery MCP run open" }, + kind: "input.start", + requestId: "request-input-mcp-recovery-run", + runId: RECOVERY_RUN_ID, + }); + await controller.waitForEvent( + (event) => event.kind === "run.started" && event.runId === RECOVERY_RUN_ID, + recoveryRunEventIndex, + 10_000, + "recovery MCP run start", + ); + + controller.enqueue( + mcpCommand("mcp-after-cancel", "echo", '{"value":"recovered"}', RECOVERY_RUN_ID), + ); expect(await controller.waitForCommandTerminal("mcp-after-cancel", 10_000)).toMatchObject({ result: { outputText: "echo:recovered" }, status: "completed", @@ -488,6 +544,7 @@ artifactTest( commandId: "changed-replay", kind: "turn.cancel", reason: "first reason", + runId: RECOVERY_RUN_ID, }); expect((await controller.waitForCommandTerminal("changed-replay", 10_000)).status).toBe( "completed", @@ -496,6 +553,7 @@ artifactTest( commandId: "changed-replay", kind: "turn.cancel", reason: "changed reason", + runId: RECOVERY_RUN_ID, }); const replayFailure = await controller.waitForRunTerminal(10_000); expect(replayFailure.status).toBe("failed"); @@ -508,6 +566,7 @@ artifactTest( expect(sessions.size).toBeGreaterThan(1); expect(unauthorizedRequests).toBe(0); } finally { + hangReleased.resolve(); await controller?.dispose(); await server.stop(true); await rm(rootPath, { force: true, recursive: true }); diff --git a/tests/driver-artifact-test-controller.test.ts b/tests/driver-artifact-test-controller.test.ts index d40f8fe..63239ec 100644 --- a/tests/driver-artifact-test-controller.test.ts +++ b/tests/driver-artifact-test-controller.test.ts @@ -3,6 +3,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { DRIVER_PROTOCOL_VERSION } from "../src/protocol/boot"; import { ForbiddenSecretScanner, DriverArtifactTestController, @@ -40,7 +41,7 @@ await rpc("/driver/hello", { capabilities: [], driverVersion: "test", pid: process.pid, - protocolVersion: 2, + protocolVersion: ${String(DRIVER_PROTOCOL_VERSION)}, runtime: payload.runtime, startedAt: new Date().toISOString(), }); @@ -70,7 +71,6 @@ if (process.env.TEST_MODE === "terminal") { await rpc("/driver/commandUpdate", { commandId: "command-1", driverInstanceId: payload.driverInstanceId, - result: null, status: "completed", }); } @@ -259,20 +259,29 @@ describe("driver artifact test controller", () => { const hello = parseDriverHelloInput({ capabilities: [{ details: undefined, id: "input_start", status: "supported", version: 1 }], driverVersion: "test", - ignored: true, pid: 1, - protocolVersion: 2, + protocolVersion: DRIVER_PROTOCOL_VERSION, runtime: "acp-fallback", startedAt: "now", }); - expect(Object.hasOwn(hello, "ignored")).toBeFalse(); expect(Object.hasOwn(hello.capabilities[0]!, "details")).toBeFalse(); + expect(() => + parseDriverHelloInput({ + capabilities: [], + driverVersion: "test", + ignored: true, + pid: 1, + protocolVersion: DRIVER_PROTOCOL_VERSION, + runtime: "acp-fallback", + startedAt: "now", + }), + ).toThrow("ignored"); expect(() => parseDriverHelloInput({ capabilities: [], driverVersion: "test", pid: 0, - protocolVersion: 2, + protocolVersion: DRIVER_PROTOCOL_VERSION, runtime: "acp-fallback", startedAt: "now", }), @@ -283,21 +292,22 @@ describe("driver artifact test controller", () => { driverInstanceId: "driver-1", status: "invented", }), - ).toThrow("status is not a supported runtime command status"); - expect( - parseDriverCommandUpdateInput({ - commandId: "command-1", - driverInstanceId: "driver-1", - result: { - isError: true, - outputText: "failed", - requestId: "request-1", - serverId: "server-1", - toolName: "tool-1", - }, - status: "completed", - }).result, - ).toMatchObject({ isError: true }); + ).toThrow("accepted' | 'cancelled' | 'completed' | 'failed"); + const completed = parseDriverCommandUpdateInput({ + commandId: "command-1", + driverInstanceId: "driver-1", + result: { + isError: true, + outputText: "failed", + requestId: "request-1", + serverId: "server-1", + toolName: "tool-1", + }, + status: "completed", + }); + expect(completed.status === "completed" ? completed.result : undefined).toMatchObject({ + isError: true, + }); expect(() => parseDriverCommandUpdateInput({ commandId: "command-1", @@ -305,7 +315,7 @@ describe("driver artifact test controller", () => { result: { outputText: "partial", requestId: "request-1" }, status: "completed", }), - ).toThrow("serverId"); + ).toThrow(); expect(() => parseDriverCommandUpdateInput({ commandId: "command-1", @@ -313,7 +323,7 @@ describe("driver artifact test controller", () => { result: { isError: true, requestId: "request-1" }, status: "completed", }), - ).toThrow("outputText"); + ).toThrow(); expect(() => parseDriverCommandUpdateInput({ commandId: "command-1", @@ -321,7 +331,7 @@ describe("driver artifact test controller", () => { result: { outputText: undefined, requestId: "request-1" }, status: "completed", }), - ).toThrow("outputText"); + ).toThrow(); expect(() => parseDriverHelloInput({ capabilities: [ @@ -330,7 +340,7 @@ describe("driver artifact test controller", () => { ], driverVersion: "test", pid: 1, - protocolVersion: 2, + protocolVersion: DRIVER_PROTOCOL_VERSION, runtime: "acp-fallback", startedAt: "now", }), @@ -344,12 +354,14 @@ describe("driver artifact test controller", () => { message: "failed", retryable: false, }, + runId: "run-1", }), ).toThrow("driver failure details.nested must be a primitive value"); const details = JSON.parse('{"__proto__":"preserved"}') as Record; const failure = parseDriverFailureInput({ driverInstanceId: "driver-1", error: { code: "failed", details, message: "failed", retryable: false }, + runId: "run-1", }); expect(Object.hasOwn(failure.error.details, "__proto__")).toBeTrue(); expect(() => @@ -369,7 +381,7 @@ describe("driver artifact test controller", () => { }); expect(commandUpdate.success).toBeFalse(); expect(commandUpdate.error?.issues).toContainEqual( - expect.objectContaining({ path: ["result", "serverId"] }), + expect.objectContaining({ path: ["result"] }), ); const events = driverRuntimeRpcSchemas.driver.pushEvents.input.safeParse({ diff --git a/tests/driver-artifact-test-controller.ts b/tests/driver-artifact-test-controller.ts index 4ed3c48..587b062 100644 --- a/tests/driver-artifact-test-controller.ts +++ b/tests/driver-artifact-test-controller.ts @@ -10,8 +10,8 @@ import { parseDriverCompletionInput, parseDriverEventBatchInput, parseDriverExternalToolEffectClaimInput, - parseDriverExternalToolEffectCompleteInput, - parseDriverExternalToolEffectUnknownInput, + parseDriverExternalToolEffectObserveInput, + parseDriverExternalToolEffectSettleInput, parseDriverFailureInput, parseDriverHeartbeatInput, parseDriverHelloInput, @@ -20,7 +20,12 @@ import { parseDriverReadyInput, type DriverLogEntry, } from "../src/protocol/orpc"; -import type { DriverCapability, McpExecuteCommandResult } from "../src/runtime-command"; +import type { + DriverCapability, + McpExecuteCommandResult, + McpExternalToolEffectState, + RuntimeCommand, +} from "../src/runtime-command"; import { PROCESS_TREE_OWNER_ENV } from "../src/runtimes/child-process"; import { AGENT_DRIVER_PROVIDER_REGISTRY, @@ -49,11 +54,7 @@ export interface DriverArtifactTestCommandUpdate { readonly status: string; } -export interface DriverArtifactTestCommand { - readonly commandId: string; - readonly kind: string; - readonly [key: string]: unknown; -} +export type DriverArtifactTestCommand = RuntimeCommand; export interface DriverArtifactEventIngressGate { readonly entered: Promise; @@ -95,6 +96,7 @@ interface DriverExit { interface DriverRunTerminal { readonly error?: unknown; + readonly runId: string; readonly status: "completed" | "failed"; } @@ -106,6 +108,30 @@ interface EventIngressGateState { readonly released: Promise; } +type ArtifactExternalToolEffect = + | { readonly effectId: string; readonly kind: "intent" } + | { + readonly attempt: number; + readonly claimToken: string; + readonly effectId: string; + readonly idempotencyKey: string; + readonly kind: "claimed"; + } + | { + readonly effectId: string; + readonly kind: "succeeded"; + readonly result: McpExecuteCommandResult; + } + | { readonly effectId: string; readonly kind: "unknown" }; + +function toExternalToolEffectState(effect: ArtifactExternalToolEffect): McpExternalToolEffectState { + if (effect.kind === "claimed") { + const { claimToken: _, ...state } = effect; + return state; + } + return structuredClone(effect); +} + interface RpcRequest { readonly id: number | string; readonly input: Record; @@ -300,13 +326,14 @@ export class DriverArtifactTestController { readonly #commandUpdates: DriverArtifactTestCommandUpdate[] = []; readonly #eventIngressGates = new Set(); readonly #eventIngressObservers = new Set<(event: DriverArtifactTestEvent) => void>(); - readonly #events: DriverArtifactTestEvent[] = []; - readonly #externalToolEffects = new Map< + readonly #eventReceipts = new Map< string, - | { readonly status: "executing" } - | { readonly result: McpExecuteCommandResult; readonly status: "completed" } - | { readonly status: "unknown" } + { + readonly receipt: { readonly eventId: string; readonly seq: number; readonly type: string }; + } >(); + readonly #events: DriverArtifactTestEvent[] = []; + readonly #externalToolEffects = new Map(); readonly #expectedCapabilities: readonly DriverCapability[] | undefined; readonly #forbiddenSecrets: readonly string[]; readonly #heartbeatIntervalMs: number; @@ -905,7 +932,7 @@ export class DriverArtifactTestController { runConfig: { commandLeaseMs: 300_000, envPolicy: "strict", - eventBatchMaxSize: 256, + eventBatchMaxSize: 64, organizationPath: this.#organizationPath, }, runId: null, @@ -935,6 +962,15 @@ export class DriverArtifactTestController { const ingressWaits = new Set>(); const accepted = batch.events.map((envelope) => { + const existing = this.#eventReceipts.get(envelope.eventId); + + if (existing !== undefined) { + if (existing.receipt.type !== envelope.event.kind) { + throw new Error(`Driver reused event ID ${envelope.eventId} with a changed type.`); + } + return existing.receipt; + } + const { event } = envelope; if (event.driverInstanceId !== this.#bootPayload.driverInstanceId) { throw new Error( @@ -979,11 +1015,13 @@ export class DriverArtifactTestController { this.#terminalRunIds.add(event.runId); } this.#nextEventSeq += 1; - return { + const receipt = { eventId: envelope.eventId, seq: this.#nextEventSeq, type: envelope.event.kind, }; + this.#eventReceipts.set(envelope.eventId, { receipt }); + return receipt; }); await Promise.all(ingressWaits); return { accepted }; @@ -997,71 +1035,102 @@ export class DriverArtifactTestController { case "/driver/commandUpdate": { const update = parseDriverCommandUpdateInput(input); this.#assertDriverInstanceId(update.driverInstanceId); - this.#commandUpdates.push({ - commandId: update.commandId, - ...(update.error === undefined ? {} : { error: update.error }), - ...(update.result === undefined ? {} : { result: update.result }), - status: update.status, - }); + this.#commandUpdates.push( + update.status === "failed" + ? { + commandId: update.commandId, + error: update.error, + status: update.status, + } + : update.status === "completed" + ? { + commandId: update.commandId, + ...(update.result === undefined ? {} : { result: update.result }), + status: update.status, + } + : { commandId: update.commandId, status: update.status }, + ); return { ok: true }; } + case "/driver/observeExternalToolEffect": { + const { commandId, driverInstanceId } = parseDriverExternalToolEffectObserveInput(input); + this.#assertDriverInstanceId(driverInstanceId); + const effect = + this.#externalToolEffects.get(commandId) ?? + ({ effectId: `artifact-test-effect-${commandId}`, kind: "intent" } as const); + this.#externalToolEffects.set(commandId, effect); + return toExternalToolEffectState(effect); + } case "/driver/claimExternalToolEffect": { - const { commandId, driverInstanceId } = parseDriverExternalToolEffectClaimInput(input); + const { claimToken, commandId, driverInstanceId } = + parseDriverExternalToolEffectClaimInput(input); this.#assertDriverInstanceId(driverInstanceId); const effectId = `artifact-test-effect-${commandId}`; - const effect = this.#externalToolEffects.get(commandId); + const effect = + this.#externalToolEffects.get(commandId) ?? ({ effectId, kind: "intent" } as const); - if (effect?.status === "completed") { - return { effectId, kind: "completed", result: structuredClone(effect.result) }; + if (effect.kind === "succeeded" || effect.kind === "unknown") { + return toExternalToolEffectState(effect); } - if (effect?.status === "unknown") { - return { effectId, kind: "unknown" }; + if (effect.kind === "claimed") { + if (effect.claimToken === claimToken) { + return toExternalToolEffectState(effect); + } + const unknown = { effectId, kind: "unknown" } as const; + this.#externalToolEffects.set(commandId, unknown); + return unknown; } - this.#externalToolEffects.set(commandId, { status: "executing" }); - return { attempt: 1, effectId, idempotencyKey: effectId, kind: "execute" }; + const claimed = { + attempt: 1, + claimToken, + effectId, + idempotencyKey: effectId, + kind: "claimed", + } as const; + this.#externalToolEffects.set(commandId, claimed); + return toExternalToolEffectState(claimed); } - case "/driver/completeExternalToolEffect": { - const { commandId, driverInstanceId, result } = - parseDriverExternalToolEffectCompleteInput(input); + case "/driver/settleExternalToolEffect": { + const { claimToken, commandId, driverInstanceId, effectId, settlement } = + parseDriverExternalToolEffectSettleInput(input); this.#assertDriverInstanceId(driverInstanceId); + const current = + this.#externalToolEffects.get(commandId) ?? ({ effectId, kind: "intent" } as const); - const current = this.#externalToolEffects.get(commandId); - if (current?.status === "completed") { - if (JSON.stringify(current.result) !== JSON.stringify(result)) { - throw new Error(`External tool effect ${commandId} completed with a different result.`); - } - return { ok: true }; + if (current.effectId !== effectId) { + throw new Error(`External tool effect ${commandId} used a mismatched effect ID.`); } - if (current?.status !== "executing") { - throw new Error(`External tool effect ${commandId} is not executing.`); + if (current.kind === "succeeded" || current.kind === "unknown") { + return toExternalToolEffectState(current); } - - this.#externalToolEffects.set(commandId, { - result: structuredClone(result), - status: "completed", - }); - return { ok: true }; - } - case "/driver/markExternalToolEffectUnknown": { - const { commandId, driverInstanceId } = parseDriverExternalToolEffectUnknownInput(input); - this.#assertDriverInstanceId(driverInstanceId); - const current = this.#externalToolEffects.get(commandId); - - if (current?.status === "completed") { - throw new Error(`Completed external tool effect ${commandId} cannot become unknown.`); + if (current.kind === "intent") { + this.#externalToolEffects.set(commandId, current); + return current; + } + if (current.claimToken !== claimToken) { + const unknown = { effectId, kind: "unknown" } as const; + this.#externalToolEffects.set(commandId, unknown); + return unknown; } - this.#externalToolEffects.set(commandId, { status: "unknown" }); - return { ok: true }; + const terminal: ArtifactExternalToolEffect = + settlement.kind === "succeeded" + ? { effectId, kind: "succeeded", result: structuredClone(settlement.result) } + : { effectId, kind: "unknown" }; + this.#externalToolEffects.set(commandId, terminal); + return toExternalToolEffectState(terminal); } case "/driver/completeRun": { const completion = parseDriverCompletionInput(input); this.#assertDriverInstanceId(completion.driverInstanceId); + if (!this.#knownRunIds.has(completion.runId)) { + throw new Error(`Driver completed unknown run ${completion.runId}.`); + } if (this.#runTerminals.length > 0) { throw new Error("Driver emitted more than one control-plane run terminal."); } - const terminal = { status: "completed" } as const; + const terminal = { runId: completion.runId, status: "completed" } as const; this.#runTerminals.push(terminal); for (const observer of this.#runTerminalIngressObservers) { observer(terminal); @@ -1071,10 +1140,13 @@ export class DriverArtifactTestController { case "/driver/failRun": { const failure = parseDriverFailureInput(input); this.#assertDriverInstanceId(failure.driverInstanceId); + if (!this.#knownRunIds.has(failure.runId)) { + throw new Error(`Driver failed unknown run ${failure.runId}.`); + } if (this.#runTerminals.length > 0) { throw new Error("Driver emitted more than one control-plane run terminal."); } - const terminal = { error: failure.error, status: "failed" } as const; + const terminal = { error: failure.error, runId: failure.runId, status: "failed" } as const; this.#runTerminals.push(terminal); for (const observer of this.#runTerminalIngressObservers) { observer(terminal); @@ -1147,7 +1219,9 @@ export class DriverArtifactTestController { #throwProtocolError(label: string): void { if (this.#protocolError !== null) { - throw new Error(`Driver protocol failed while waiting for ${label}: ${this.#protocolError}.`); + throw new Error( + `Driver protocol failed while waiting for ${label}: ${this.#protocolError}.\n${this.diagnostics()}`, + ); } } @@ -1179,7 +1253,7 @@ export class DriverArtifactTestController { waitForRunTerminal( timeoutMs: number, fromIndex = 0, - ): Promise<{ error?: unknown; status: "completed" | "failed" }> { + ): Promise<{ error?: unknown; runId: string; status: "completed" | "failed" }> { return this.#waitFor( () => this.#runTerminals.slice(fromIndex).at(-1), "control-plane run terminal", diff --git a/tests/driver-boot-payload-fixture.ts b/tests/driver-boot-payload-fixture.ts index df9c67b..99b805b 100644 --- a/tests/driver-boot-payload-fixture.ts +++ b/tests/driver-boot-payload-fixture.ts @@ -7,7 +7,7 @@ import type { SandboxId, SandboxSessionId, } from "../src/protocol/boot"; -import { DRIVER_CONTROL_PORT_MIN } from "../src/protocol/boot"; +import { DRIVER_CONTROL_PORT_MIN, DRIVER_PROTOCOL_VERSION } from "../src/protocol/boot"; import type { DriverInstanceId, RunId, SessionId } from "../src/protocol/id"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; @@ -85,7 +85,7 @@ export const driverBootPayload = { skills: [], }, heartbeatIntervalMs: 1_000, - protocolVersion: 2, + protocolVersion: DRIVER_PROTOCOL_VERSION, runtime: "openai-runtime", runtimeTransport: "openai-app-server", sandboxId: DRIVER_TEST_IDS.sandboxId, diff --git a/tests/driver-boot-schema.test.ts b/tests/driver-boot-schema.test.ts index 08ef117..9a35f40 100644 --- a/tests/driver-boot-schema.test.ts +++ b/tests/driver-boot-schema.test.ts @@ -267,4 +267,17 @@ describe("Driver boot schema", () => { TypeError, ); }); + + test.each([ + "", + "00-00000000000000000000000000000000-0000000000000001-01", + "00-00000000000000000000000000000001-0000000000000000-01", + "00-0000000000000000000000000000000g-0000000000000001-01", + "00-0000000000000000000000000000000A-0000000000000001-01", + "00-00000000000000000000000000000001-0000000000000001", + ])("rejects invalid W3C traceparent %p", (traceparent) => { + expect(() => parseDriverBootPayload({ ...driverBootPayload, traceparent })).toThrow( + "traceparent", + ); + }); }); diff --git a/tests/driver-event-publisher-admission.test.ts b/tests/driver-event-publisher-admission.test.ts index a9da097..dd8dd22 100644 --- a/tests/driver-event-publisher-admission.test.ts +++ b/tests/driver-event-publisher-admission.test.ts @@ -35,7 +35,7 @@ function acceptEvents( ): DriverEventBatchOutput { return { accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, + eventId: event.sourceEventId!, seq: sequence(index), type: event.kind, })), @@ -43,6 +43,190 @@ function acceptEvents( } describe("DriverEventPublisher", () => { + test("joins an unresolved lossless draft by explicit identity", async () => { + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (attempts.length === 1) { + throw new Error("response lost"); + } + + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const draft = { + ...createEvent("message.completed"), + sourceEventId: "stable-source-event-id", + }; + + await expect(publisher.push(context, "first", [draft])).rejects.toThrow("response lost"); + await expect( + publisher.push(context, "retry", [structuredClone(draft)]), + ).resolves.toBeUndefined(); + + expect(kinds(attempts)).toEqual([["message.completed"], ["message.completed"]]); + expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); + }); + + test("keeps distinct implicit drafts separate when their content matches", async () => { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + if (attempts.length === 1) { + entered.resolve(); + await release.promise; + } + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const first = publisher.push(context, "first", [createEvent("message.completed")]); + + await entered.promise; + const second = publisher.push(context, "second", [createEvent("message.completed")]); + release.resolve(); + await Promise.all([first, second]); + + expect(kinds(attempts)).toEqual([["message.completed"], ["message.completed"]]); + expect(attempts[1]?.[0]?.sourceEventId).not.toBe(attempts[0]?.[0]?.sourceEventId); + }); + + test("joins a retained identity while admitting a fresh event", async () => { + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + if (attempts.length === 1) { + throw new Error("response lost"); + } + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const retained = { + ...createEvent("message.started"), + sourceEventId: "retained-source-event-id", + }; + + await expect(publisher.push(context, "first", [retained])).rejects.toThrow("response lost"); + await publisher.push(context, "retry", [retained, createEvent("message.completed")]); + + expect(kinds(attempts)).toEqual([ + ["message.started"], + ["message.started", "message.completed"], + ]); + expect(attempts[1]?.[0]?.sourceEventId).toBe(attempts[0]?.[0]?.sourceEventId); + }); + + test("settles an identity join when its event is acked before a blocked suffix", async () => { + const prefixEntered = Promise.withResolvers(); + const releasePrefix = Promise.withResolvers(); + const suffixEntered = Promise.withResolvers(); + const releaseSuffix = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + if (attempts.length === 1) { + prefixEntered.resolve(); + await releasePrefix.promise; + return { accepted: [acceptEvents(events).accepted[0]!] }; + } + suffixEntered.resolve(); + await releaseSuffix.promise; + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const draft = { + ...createEvent("message.started"), + sourceEventId: "joined-source-event-id", + }; + const first = publisher.push(context, "first", [draft, createEvent("message.completed")]); + + await prefixEntered.promise; + let joinedSettled = false; + const joined = publisher.push(context, "join", [draft]).then(() => { + joinedSettled = true; + }); + releasePrefix.resolve(); + await suffixEntered.promise; + await Bun.sleep(0); + expect(joinedSettled).toBe(true); + releaseSuffix.resolve(); + await Promise.all([first, joined]); + + expect(kinds(attempts)).toEqual([ + ["message.started", "message.completed"], + ["message.completed"], + ]); + }); + + test("rejects changed content that reuses an unresolved source ID", async () => { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + entered.resolve(); + await release.promise; + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const first = publisher.push(context, "first", [ + { ...createEvent("message.started"), sourceEventId: "stable-source-event-id" }, + ]); + + await entered.promise; + await expect( + publisher.push(context, "conflict", [ + { ...createEvent("message.completed"), sourceEventId: "stable-source-event-id" }, + ]), + ).rejects.toThrow("source event ID conflicts"); + release.resolve(); + await first; + + expect(kinds(attempts)).toEqual([["message.started"]]); + }); + + test("propagates an in-flight rejection to an identity join", async () => { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + entered.resolve(); + await release.promise; + throw new DriverEventRejectedError(events[0]!.sourceEventId!, new Error("rejected")); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const draft = { + ...createEvent("message.completed"), + sourceEventId: "stable-source-event-id", + }; + const first = publisher.push(context, "first", [draft]); + + await entered.promise; + const joined = publisher.push(context, "join", [structuredClone(draft)]); + void first.catch(() => {}); + void joined.catch(() => {}); + release.resolve(); + + await expect(first).rejects.toThrow("rejected"); + await expect(joined).rejects.toThrow("rejected"); + expect(kinds(attempts)).toEqual([["message.completed"]]); + }); + test("keeps an explicit source ID across direct lossless retries", async () => { const attempts: DriverEventInput[][] = []; const events = withSourceEventIds([createEvent("message.completed")]); @@ -76,7 +260,7 @@ describe("DriverEventPublisher", () => { return { accepted: [ { - eventId: events[0]!.sourceEventId, + eventId: events[0]!.sourceEventId!, seq: ++sequence, type: events[0]!.kind, }, @@ -134,7 +318,7 @@ describe("DriverEventPublisher", () => { return { accepted: [ { - eventId: events[0]?.sourceEventId, + eventId: events[0]!.sourceEventId!, seq: 1, type: events[0]!.kind, }, @@ -179,6 +363,7 @@ describe("DriverEventPublisher", () => { return { accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: 40 + index, type: event.kind, })), @@ -778,7 +963,7 @@ describe("DriverEventPublisher", () => { return { accepted: [ { - eventId: events[0]!.sourceEventId, + eventId: events[0]!.sourceEventId!, seq: (seq += 1), type: events[0]!.kind, }, @@ -1351,6 +1536,47 @@ describe("DriverEventPublisher", () => { ]); }); + test("rejects explicit late events after their run terminal is acknowledged", async () => { + const attempts: DriverEventInput[][] = []; + let activeRunId: RunId | null = DRIVER_TEST_IDS.runId; + const context = createContext({ + currentRunId: () => activeRunId, + pushEvents: async (events) => { + attempts.push(events); + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + + await publisher.pushTerminal(context, "terminal", [], createRunTerminal("run.completed")); + activeRunId = null; + + const lateEvents = [ + { ...createEvent("message.completed"), runId: DRIVER_TEST_IDS.runId }, + { + kind: "tool.call.updated" as const, + payload: { status: "completed", toolCallId: "tool-late" }, + runId: DRIVER_TEST_IDS.runId, + }, + { + kind: "file.changed" as const, + payload: { change: "upsert", path: "late.txt" }, + runId: DRIVER_TEST_IDS.runId, + }, + ]; + + for (const event of lateEvents) { + await expect(publisher.push(context, "late.lossless", [event])).rejects.toThrow( + "must target the active run", + ); + await expect( + publisher.push(context, "late.best-effort", [{ ...event, delivery: "best_effort" }]), + ).resolves.toBeUndefined(); + } + + expect(kinds(attempts)).toEqual([["run.completed"]]); + }); + test("keeps session events unscoped without reopening a settled run", async () => { const attempts: DriverEventInput[][] = []; let activeRunId: RunId | null = DRIVER_TEST_IDS.runId; diff --git a/tests/driver-event-publisher-backpressure.test.ts b/tests/driver-event-publisher-backpressure.test.ts index b1dfcf2..b48d8c5 100644 --- a/tests/driver-event-publisher-backpressure.test.ts +++ b/tests/driver-event-publisher-backpressure.test.ts @@ -13,7 +13,7 @@ describe("DriverEventPublisher", () => { const accepted = attempts.length === 1 ? events.slice(0, 1) : []; return { accepted: accepted.map((event, index) => ({ - eventId: event.sourceEventId, + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), @@ -60,7 +60,7 @@ describe("DriverEventPublisher", () => { recovered.resolve(); return { accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), diff --git a/tests/driver-event-publisher-drain.test.ts b/tests/driver-event-publisher-drain.test.ts index abc168f..63a3877 100644 --- a/tests/driver-event-publisher-drain.test.ts +++ b/tests/driver-event-publisher-drain.test.ts @@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; import type { DriverEventInput } from "../src/protocol/events"; -import { isDriverId } from "../src/protocol/id"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; import { createContext, createDelta, createEvent, kinds } from "./driver-event-publisher-fixture"; @@ -25,6 +24,7 @@ describe("DriverEventPublisher", () => { const acceptedEvents = attempts.length === 1 ? events.slice(0, 1) : events; return { accepted: acceptedEvents.map((event) => ({ + eventId: event.sourceEventId!, seq: nextSeq++, type: event.kind, })), @@ -64,6 +64,7 @@ describe("DriverEventPublisher", () => { return { accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: 50 + index, type: event.kind, })), @@ -94,7 +95,7 @@ describe("DriverEventPublisher", () => { if (attempts.length === 1) { return { - accepted: [{ seq: 40, type: events[0]!.kind }], + accepted: [{ eventId: events[0]!.sourceEventId!, seq: 40, type: events[0]!.kind }], }; } @@ -104,6 +105,7 @@ describe("DriverEventPublisher", () => { return { accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: 50 + index, type: event.kind, })), @@ -145,6 +147,7 @@ describe("DriverEventPublisher", () => { return { accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: attempts.length * 2_000 + index, type: event.kind, })), @@ -194,7 +197,11 @@ describe("DriverEventPublisher", () => { } return { - accepted: events.map((event, index) => ({ seq: index + 1, type: event.kind })), + accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, + seq: index + 1, + type: event.kind, + })), }; }, }); @@ -233,7 +240,7 @@ describe("DriverEventPublisher", () => { return { accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), @@ -332,7 +339,7 @@ describe("DriverEventPublisher", () => { return { accepted: [ { - eventId: events[1]?.sourceEventId, + eventId: events[1]!.sourceEventId!, seq: 1, type: events[0]!.kind, }, @@ -342,7 +349,7 @@ describe("DriverEventPublisher", () => { return { accepted: events.map((event, index) => ({ - eventId: event.sourceEventId, + eventId: event.sourceEventId!, seq: index + 2, type: event.kind, })), @@ -357,27 +364,6 @@ describe("DriverEventPublisher", () => { expect(attempt).toBe(2); }); - test("gives a reused draft object a new identity after a successful push", async () => { - const attempts: DriverEventInput[][] = []; - const context = createContext({ - pushEvents: async (events) => { - attempts.push(events); - return { - accepted: events.map((event, index) => ({ seq: index + 1, type: event.kind })), - }; - }, - }); - const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); - const event = createEvent("message.completed"); - - await publisher.push(context, "first", [event]); - await publisher.push(context, "second", [event]); - - expect(attempts[1]?.[0]?.sourceEventId).not.toBe(attempts[0]?.[0]?.sourceEventId); - expect(isDriverId(attempts[0]?.[0]?.sourceEventId)).toBe(true); - expect(isDriverId(attempts[1]?.[0]?.sourceEventId)).toBe(true); - }); - test.each([ [ "too many", @@ -411,11 +397,19 @@ describe("DriverEventPublisher", () => { attempts.push(events); if (attempts.length === 1) { - return { accepted: malformedReceipts }; + return { + accepted: malformedReceipts.map( + (receipt: { readonly seq: number; readonly type: string }, index: number) => ({ + ...receipt, + eventId: events[index]?.sourceEventId ?? "extra-event-id", + }), + ), + }; } return { accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: 50 + index, type: event.kind, })), diff --git a/tests/driver-external-tool-effect-v3.test.ts b/tests/driver-external-tool-effect-v3.test.ts new file mode 100644 index 0000000..63f68dd --- /dev/null +++ b/tests/driver-external-tool-effect-v3.test.ts @@ -0,0 +1,1149 @@ +import { describe, expect, test } from "bun:test"; + +import { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; +import type { DriverEventInput } from "../src/protocol/events"; +import { driverRuntimeRpcSchemas } from "../src/protocol/orpc"; +import type { + McpExecuteCommand, + McpExternalToolEffectState, + McpExternalToolExecutionResult, +} from "../src/runtime-command"; +import { RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES } from "../src/runtime-command"; +import { + DRIVER_TEST_IDS, + FakeDriverRuntimeIo, + createBackend, + createDispatcher, + waitForUpdate, +} from "./driver-runtime-boundary-fixtures"; + +function command(commandId: string): McpExecuteCommand { + return { + argumentsJson: '{"title":"once"}', + commandId, + kind: "mcp.execute", + requestId: `request-${commandId}`, + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: `tool-${commandId}`, + toolName: "createIssue", + }; +} + +async function waitFor(predicate: () => boolean): Promise { + for (let attempt = 0; attempt < 100; attempt += 1) { + if (predicate()) { + return; + } + await Bun.sleep(0); + } + throw new Error("Timed out waiting for Driver MCP state."); +} + +describe("durable external MCP effect protocol v3", () => { + test("exposes only the schema-first v3 wire", () => { + const claimToken = "00000000-0000-4000-8000-000000000001"; + const result = { + outputText: "stored", + requestId: "request-schema", + serverId: "server-schema", + toolName: "lookup", + }; + + expect( + driverRuntimeRpcSchemas.driver.observeExternalToolEffect.output.parse({ + effectId: "effect-schema", + kind: "intent", + }), + ).toEqual({ effectId: "effect-schema", kind: "intent" }); + expect( + driverRuntimeRpcSchemas.driver.claimExternalToolEffect.output.parse({ + attempt: 1, + effectId: "effect-schema", + idempotencyKey: "idempotency-schema", + kind: "claimed", + }), + ).toMatchObject({ kind: "claimed" }); + expect( + driverRuntimeRpcSchemas.driver.settleExternalToolEffect.input.parse({ + claimToken, + commandId: "command-schema", + driverInstanceId: "driver-schema", + effectId: "effect-schema", + settlement: { kind: "succeeded", providerReceiptJson: null, result }, + }), + ).toMatchObject({ settlement: { kind: "succeeded", result } }); + for (const invalidClaimToken of [ + "", + "not-a-uuid", + "00000000-0000-4000-8000-ABCDEFABCDEF", + "00000000-0000-f000-8000-000000000001", + "00000000-0000-4000-c000-000000000001", + ]) { + expect( + driverRuntimeRpcSchemas.driver.claimExternalToolEffect.input.safeParse({ + claimToken: invalidClaimToken, + commandId: "command-schema", + driverInstanceId: "driver-schema", + }).success, + ).toBeFalse(); + expect( + driverRuntimeRpcSchemas.driver.settleExternalToolEffect.input.safeParse({ + claimToken: invalidClaimToken, + commandId: "command-schema", + driverInstanceId: "driver-schema", + effectId: "effect-schema", + settlement: { kind: "unknown" }, + }).success, + ).toBeFalse(); + } + expect( + driverRuntimeRpcSchemas.driver.claimExternalToolEffect.output.safeParse({ + attempt: 1, + effectId: "effect-schema", + idempotencyKey: "idempotency-schema", + kind: "execute", + }).success, + ).toBeFalse(); + expect("completeExternalToolEffect" in driverRuntimeRpcSchemas.driver).toBeFalse(); + expect("markExternalToolEffectUnknown" in driverRuntimeRpcSchemas.driver).toBeFalse(); + }); + + test.each([ + ["stale", DRIVER_TEST_IDS.secondRunId, DRIVER_TEST_IDS.runId], + ["future", DRIVER_TEST_IDS.runId, DRIVER_TEST_IDS.secondRunId], + ] as const)( + "rejects a %s-run command before observing an external effect", + async (_case, activeRunId, commandRunId) => { + const input = { ...command(`wrong-run-${_case}`), runId: commandRunId }; + const socket = new FakeDriverRuntimeIo([input], activeRunId); + let claims = 0; + let observations = 0; + let preparations = 0; + socket.claimExternalToolEffect = async () => { + claims += 1; + throw new Error("unexpected claim"); + }; + socket.observeExternalToolEffect = async () => { + observations += 1; + throw new Error("unexpected observation"); + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => { + preparations += 1; + throw new Error("unexpected preparation"); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + + expect({ claims, observations, preparations }).toEqual({ + claims: 0, + observations: 0, + preparations: 0, + }); + expect(socket.updates.at(-1)).toMatchObject({ + commandId: input.commandId, + error: { code: "driver.command_failed.mcp.execute" }, + status: "failed", + }); + }, + ); + + test("normalizes an oversized MCP failure before emitting its tool event", async () => { + const input = command("oversized-failure"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => { + throw new Error("界".repeat(500_000)); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate(socket, (update) => update.status === "failed"); + + const terminal = socket.updates.at(-1); + const failedToolEvent = socket.pushedEvents + .flatMap(({ events }) => events) + .find( + (event) => + event.kind === "tool.call.updated" && + typeof event.payload === "object" && + event.payload !== null && + Reflect.get(event.payload, "status") === "failed", + ); + expect(terminal).toMatchObject({ + error: { code: "driver.error_oversized" }, + status: "failed", + }); + expect(failedToolEvent).toMatchObject({ + payload: { rawOutput: terminal?.status === "failed" ? terminal.error.message : undefined }, + }); + }); + + test("runs the protocol in durable order and completes from canonical settlement", async () => { + const input = command("ordered"); + const trace: string[] = []; + const claims: Parameters[0][] = []; + const settlements: Parameters[0][] = []; + + class OrderedSocket extends FakeDriverRuntimeIo { + override async observeExternalToolEffect() { + trace.push("observe"); + return { effectId: "effect-ordered", kind: "intent" as const }; + } + + override async claimExternalToolEffect( + claim: Parameters[0], + ) { + trace.push("claim"); + claims.push(structuredClone(claim)); + return { + attempt: 1, + effectId: "effect-ordered", + idempotencyKey: "idempotency-ordered", + kind: "claimed" as const, + }; + } + + override async settleExternalToolEffect( + settlement: Parameters[0], + ) { + trace.push("settle"); + settlements.push(structuredClone(settlement)); + return settlement.settlement.kind === "succeeded" + ? { + effectId: settlement.effectId, + kind: "succeeded" as const, + result: settlement.settlement.result, + } + : { effectId: settlement.effectId, kind: "unknown" as const }; + } + + override async pushEvents( + batch: Parameters[0], + ): ReturnType { + for (const event of batch.events) { + if ( + event.kind === "tool.call.updated" && + typeof event.payload === "object" && + event.payload !== null && + "status" in event.payload && + typeof event.payload.status === "string" + ) { + trace.push(event.payload.status); + } + } + return super.pushEvents(batch); + } + } + + const socket = new OrderedSocket([input], DRIVER_TEST_IDS.runId); + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => { + trace.push("prepare"); + return { + async execute() { + trace.push("execute"); + return { + outputText: "created A-1", + providerReceiptJson: '{"receipt":"A-1"}', + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + }, + async [Symbol.asyncDispose]() { + trace.push("cleanup"); + }, + }; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate(socket, (update) => update.status === "completed"); + + expect(trace).toEqual([ + "running", + "observe", + "prepare", + "claim", + "execute", + "settle", + "cleanup", + "completed", + ]); + expect(claims[0]?.claimToken).toMatch(/^[0-9a-f-]{36}$/u); + expect(settlements[0]).toMatchObject({ + claimToken: claims[0]?.claimToken, + commandId: input.commandId, + effectId: "effect-ordered", + settlement: { + kind: "succeeded", + providerReceiptJson: '{"receipt":"A-1"}', + }, + }); + expect( + socket.pushedEvents + .flatMap(({ events }) => events) + .filter((event) => event.kind === "tool.call.updated") + .map(({ correlationId }) => correlationId), + ).toEqual([input.commandId, input.commandId]); + }); + + test("settles a known oversized provider result as bounded succeeded data", async () => { + const input = command("bounded-success"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const settlements: Parameters[0][] = []; + socket.settleExternalToolEffect = async (next) => { + settlements.push(structuredClone(next)); + return next.settlement.kind === "succeeded" + ? { + effectId: next.effectId, + kind: "succeeded", + result: next.settlement.result, + } + : { effectId: next.effectId, kind: "unknown" }; + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpExecute: async () => ({ + outputText: "界".repeat(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES), + providerReceiptJson: JSON.stringify({ diagnostic: "unused" }), + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate(socket, (update) => update.status === "completed"); + + expect(settlements[0]?.settlement).toEqual({ + kind: "succeeded", + result: { + isError: true, + outputText: + "MCP tool output was omitted because its durable settlement exceeded the 1044480-byte limit.", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }, + }); + expect(socket.updates.at(-1)).toMatchObject({ + result: { isError: true, outputText: expect.stringContaining("output was omitted") }, + status: "completed", + }); + }); + + test("rejects an oversized command before acknowledgement, claim, or provider preparation", async () => { + const input = { + ...command("oversized-identity"), + requestId: "r".repeat(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES), + }; + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + let claims = 0; + let preparations = 0; + socket.claimExternalToolEffect = async (...arguments_) => { + claims += 1; + return FakeDriverRuntimeIo.prototype.claimExternalToolEffect.apply(socket, arguments_); + }; + const runtime = createDispatcher({ + backend: createBackend(), + mcpPrepare: async () => { + preparations += 1; + throw new Error("oversized identity must not prepare the provider"); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await expect(runtime.dispatcher.run(socket, runtime.logger)).rejects.toThrow( + "Runtime command exceeds", + ); + + expect(claims).toBe(0); + expect(preparations).toBe(0); + expect(socket.updates).toEqual([]); + }); + + test("replays a durable succeeded effect after completed event delivery fails", async () => { + const input = command("completed-event-replay"); + const result = { + outputText: "created once", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + let state: McpExternalToolEffectState = { + effectId: `test-effect-${input.commandId}`, + kind: "intent", + }; + let executions = 0; + const firstEventIds: string[] = []; + const firstSocket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + firstSocket.observeExternalToolEffect = async () => state; + firstSocket.settleExternalToolEffect = async (settlement) => { + state = { effectId: settlement.effectId, kind: "succeeded", result }; + return state; + }; + const firstPush = firstSocket.pushEvents.bind(firstSocket); + firstSocket.pushEvents = async (batch) => { + const completed = batch.events.find( + (event) => + event.kind === "tool.call.updated" && + typeof event.payload === "object" && + event.payload !== null && + Reflect.get(event.payload, "status") === "completed", + ); + if (completed !== undefined) { + firstEventIds.push(completed.sourceEventId!); + throw new Error("completed event acknowledgement lost"); + } + return firstPush(batch); + }; + let firstRuntime!: ReturnType; + firstRuntime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => firstRuntime.shutdownCalls.length > 0, + mcpExecute: async () => { + executions += 1; + return result; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const firstRun = firstRuntime.dispatcher.run(firstSocket, firstRuntime.logger); + await waitFor(() => firstRuntime.shutdownCalls.includes("driver.mcp_task_failed")); + await firstRun.catch(() => {}); + + expect(executions).toBe(1); + expect(firstSocket.updates).toEqual([{ commandId: input.commandId, status: "accepted" }]); + expect(firstEventIds).toEqual([`mcp.execute.completed:${input.commandId}`]); + + const replacementEventIds: string[] = []; + const replacementSocket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + replacementSocket.observeExternalToolEffect = async () => state; + const replacementPush = replacementSocket.pushEvents.bind(replacementSocket); + replacementSocket.pushEvents = async (batch) => { + for (const event of batch.events) { + if ( + event.kind === "tool.call.updated" && + typeof event.payload === "object" && + event.payload !== null && + Reflect.get(event.payload, "status") === "completed" + ) { + replacementEventIds.push(event.sourceEventId!); + } + } + return replacementPush(batch); + }; + const replacement = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => replacementSocket.isDrained(), + mcpPrepare: async () => { + throw new Error("durable succeeded effect must not re-execute"); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await replacement.dispatcher.run(replacementSocket, replacement.logger); + await waitForUpdate(replacementSocket, (update) => update.status === "completed"); + + expect(executions).toBe(1); + expect(replacementEventIds).toEqual(firstEventIds); + expect(replacementSocket.updates.at(-1)).toMatchObject({ + result, + status: "completed", + }); + }); + + test("keeps an MCP command accepted when its failed tool event is not durable", async () => { + const input = command("failed-event-unavailable"); + const eventIds: string[] = []; + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const push = socket.pushEvents.bind(socket); + socket.pushEvents = async (batch) => { + const failed = batch.events.find( + (event) => + event.kind === "tool.call.updated" && + typeof event.payload === "object" && + event.payload !== null && + Reflect.get(event.payload, "status") === "failed", + ); + if (failed !== undefined) { + eventIds.push(failed.sourceEventId!); + throw new Error("failed event unavailable"); + } + return push(batch); + }; + let runtime!: ReturnType; + runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => runtime.shutdownCalls.length > 0, + mcpPrepare: async () => { + throw new Error("provider preparation failed"); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const run = runtime.dispatcher.run(socket, runtime.logger); + await waitFor(() => runtime.shutdownCalls.includes("driver.mcp_task_failed")); + await run.catch(() => {}); + + expect(eventIds).toHaveLength(1); + expect(eventIds[0]).toMatch(/^mcp\.execute\.failed:[0-9a-f]{64}$/u); + expect(socket.updates).toEqual([{ commandId: input.commandId, status: "accepted" }]); + }); + + test("content-addresses changed MCP failures across an ACK-lost instance replay", async () => { + const input = command("failed-event-cross-instance"); + const persisted = new Map(); + + const observeFailure = async (message: string, loseAck: boolean) => { + const observed = Promise.withResolvers(); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const push = socket.pushEvents.bind(socket); + socket.pushEvents = async (batch) => { + const failed = batch.events.find( + (event) => + event.kind === "tool.call.updated" && + typeof event.payload === "object" && + event.payload !== null && + Reflect.get(event.payload, "status") === "failed", + ); + if (failed === undefined) { + return push(batch); + } + + const sourceEventId = failed.sourceEventId!; + const content = JSON.stringify(failed); + const previous = persisted.get(sourceEventId); + expect(previous === undefined || previous === content).toBeTrue(); + persisted.set(sourceEventId, content); + observed.resolve(structuredClone(failed)); + const result = await push(batch); + if (loseAck) { + throw new Error("failed event ACK lost after persistence"); + } + return result; + }; + let runtime!: ReturnType; + runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => (loseAck ? runtime.shutdownCalls.length > 0 : socket.isDrained()), + mcpPrepare: async () => { + throw new Error(message); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const run = runtime.dispatcher.run(socket, runtime.logger); + const event = await observed.promise; + await waitFor(() => + loseAck + ? runtime.shutdownCalls.includes("driver.mcp_task_failed") + : socket.updates.some( + ({ commandId, status }) => commandId === input.commandId && status === "failed", + ), + ); + await run.catch(() => {}); + return event; + }; + + const first = await observeFailure("same failure", true); + const replay = await observeFailure("same failure", false); + const changed = await observeFailure("changed failure", false); + + expect(replay.sourceEventId).toBe(first.sourceEventId); + expect(changed.sourceEventId).not.toBe(first.sourceEventId); + expect([first, replay, changed].map(({ correlationId }) => correlationId)).toEqual([ + input.commandId, + input.commandId, + input.commandId, + ]); + expect(persisted.size).toBe(2); + }); + + test("keeps an aborted MCP command accepted when its cancelled tool event is not durable", async () => { + const input = command("cancelled-event-unavailable"); + const observed = Promise.withResolvers(); + const eventIds: string[] = []; + const shutdown = new AbortController(); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + socket.observeExternalToolEffect = async (_command, signal) => { + observed.resolve(); + return new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => reject(signal.reason), { once: true }); + }); + }; + const push = socket.pushEvents.bind(socket); + socket.pushEvents = async (batch) => { + const cancelled = batch.events.find( + (event) => + event.kind === "tool.call.updated" && + typeof event.payload === "object" && + event.payload !== null && + Reflect.get(event.payload, "status") === "cancelled", + ); + if (cancelled !== undefined) { + eventIds.push(cancelled.sourceEventId!); + throw new Error("cancelled event unavailable"); + } + return push(batch); + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => shutdown.signal.aborted, + runtimeState: new DriverRuntimeStateMachine("ready"), + shutdownSignal: shutdown.signal, + }); + const run = runtime.dispatcher.run(socket, runtime.logger); + + await observed.promise; + shutdown.abort(new Error("stop before claim")); + await run.catch(() => {}); + + expect(eventIds).toEqual([`mcp.execute.cancelled:${input.commandId}`]); + expect(socket.updates).toEqual([{ commandId: input.commandId, status: "accepted" }]); + }); + + test("rechecks exact run ownership after preparation and before claim", async () => { + const input = command("run-changed-before-claim"); + let currentRunId = DRIVER_TEST_IDS.runId; + let claims = 0; + let executions = 0; + class RunChangingSocket extends FakeDriverRuntimeIo { + override currentRunId() { + return currentRunId; + } + } + const socket = new RunChangingSocket([input], DRIVER_TEST_IDS.runId); + socket.claimExternalToolEffect = async (...arguments_) => { + claims += 1; + return FakeDriverRuntimeIo.prototype.claimExternalToolEffect.apply(socket, arguments_); + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => { + currentRunId = DRIVER_TEST_IDS.secondRunId; + return { + async execute() { + executions += 1; + throw new Error("stale run must not execute"); + }, + async [Symbol.asyncDispose]() {}, + }; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate(socket, (update) => update.status === "failed"); + + expect(claims).toBe(0); + expect(executions).toBe(0); + expect(socket.updates.map(({ status }) => status)).toEqual(["accepted", "failed"]); + }); + + test.each(["succeeded", "unknown", "claimed"] as const)( + "does not prepare an already-%s effect", + async (kind) => { + const input = command(`observed-${kind}`); + const result = { + outputText: "stored result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + const state: McpExternalToolEffectState = + kind === "succeeded" + ? { effectId: "effect-observed", kind, result } + : kind === "claimed" + ? { + attempt: 2, + effectId: "effect-observed", + idempotencyKey: "idempotency-observed", + kind, + } + : { effectId: "effect-observed", kind }; + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + socket.observeExternalToolEffect = async () => state; + let preparations = 0; + let claims = 0; + socket.claimExternalToolEffect = async (...arguments_) => { + claims += 1; + return FakeDriverRuntimeIo.prototype.claimExternalToolEffect.apply(socket, arguments_); + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => { + preparations += 1; + throw new Error("observed effects must not be prepared"); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const run = runtime.dispatcher.run(socket, runtime.logger); + if (kind === "succeeded") { + await waitForUpdate(socket, (update) => update.status === "completed"); + expect(socket.updates.at(-1)).toEqual({ + commandId: input.commandId, + result, + status: "completed", + }); + } else if (kind === "unknown") { + await waitForUpdate(socket, (update) => update.status === "failed"); + expect(socket.updates.at(-1)).toMatchObject({ + error: { + code: "driver.external_tool_effect_unknown", + details: { + commandId: input.commandId, + effectId: "effect-observed", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }, + retryable: false, + }, + status: "failed", + }); + } else { + await waitFor(() => runtime.shutdownCalls.includes("driver.mcp_task_failed")); + expect(socket.updates).toEqual([{ commandId: input.commandId, status: "accepted" }]); + } + await run.catch(() => {}); + expect(preparations).toBe(0); + expect(claims).toBe(0); + }, + ); + + test.each([ + ["observe", "requestId", 0, 0], + ["claim", "serverId", 1, 0], + ["settle", "toolName", 1, 1], + ] as const)( + "fail-closes a canonical result with a mismatched identity from %s", + async (source, mismatchedField, expectedPreparations, expectedExecutions) => { + const input = command(`mismatched-${source}`); + const effectId = `test-effect-${input.commandId}`; + const matchingResult = { + outputText: "canonical result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + const mismatchedState: McpExternalToolEffectState = { + effectId, + kind: "succeeded", + result: { ...matchingResult, [mismatchedField]: `wrong-${mismatchedField}` }, + }; + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + if (source === "observe") { + socket.observeExternalToolEffect = async () => mismatchedState; + } else if (source === "claim") { + socket.claimExternalToolEffect = async () => mismatchedState; + } else { + socket.settleExternalToolEffect = async () => mismatchedState; + } + + let executions = 0; + let preparations = 0; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => { + preparations += 1; + return { + async execute() { + executions += 1; + return matchingResult; + }, + async [Symbol.asyncDispose]() {}, + }; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const run = runtime.dispatcher.run(socket, runtime.logger); + await waitFor(() => runtime.shutdownCalls.includes("driver.mcp_task_failed")); + await run.catch(() => {}); + + expect(preparations).toBe(expectedPreparations); + expect(executions).toBe(expectedExecutions); + expect(socket.updates).toEqual([{ commandId: input.commandId, status: "accepted" }]); + }, + ); + + test("retries a lost claim acknowledgement with the same token", async () => { + const input = command("claim-retry"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const claims: Parameters[0][] = []; + socket.claimExternalToolEffect = async (claim) => { + claims.push(structuredClone(claim)); + if (claims.length === 1) { + throw new Error("claim acknowledgement lost"); + } + return { + attempt: 1, + effectId: `test-effect-${input.commandId}`, + idempotencyKey: "claim-retry-key", + kind: "claimed", + }; + }; + let executions = 0; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpExecute: async () => { + executions += 1; + return { + outputText: "created once", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate(socket, (update) => update.status === "completed"); + + expect(claims).toHaveLength(2); + expect(claims[1]).toEqual(claims[0]); + expect(executions).toBe(1); + }); + + test("leaves the command accepted when both claim responses are unreachable", async () => { + const input = command("claim-unreachable"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const claims: Parameters[0][] = []; + let disposals = 0; + let executions = 0; + let settlements = 0; + socket.claimExternalToolEffect = async (claim) => { + claims.push(structuredClone(claim)); + throw new Error("claim response unreachable"); + }; + socket.settleExternalToolEffect = async (...arguments_) => { + settlements += 1; + return FakeDriverRuntimeIo.prototype.settleExternalToolEffect.apply(socket, arguments_); + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => ({ + async execute() { + executions += 1; + throw new Error("unreachable claim must not execute"); + }, + async [Symbol.asyncDispose]() { + disposals += 1; + }, + }), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const run = runtime.dispatcher.run(socket, runtime.logger); + await waitFor(() => runtime.shutdownCalls.includes("driver.mcp_task_failed")); + await run.catch(() => {}); + + expect(claims).toHaveLength(2); + expect(claims[1]).toEqual(claims[0]); + expect(executions).toBe(0); + expect(settlements).toBe(0); + expect(disposals).toBe(1); + expect(socket.updates).toEqual([{ commandId: input.commandId, status: "accepted" }]); + }); + + test("settles provider failure as unknown instead of cancelling the command", async () => { + const input = command("provider-unknown"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const settlements: Parameters[0][] = []; + socket.settleExternalToolEffect = async (settlement, signal) => { + expect(signal.aborted).toBeFalse(); + settlements.push(structuredClone(settlement)); + return { effectId: settlement.effectId, kind: "unknown" }; + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpExecute: async () => { + throw new Error("provider response lost"); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate(socket, (update) => update.status === "failed"); + + expect(settlements).toHaveLength(1); + expect(settlements[0]?.settlement).toEqual({ kind: "unknown" }); + expect(socket.updates.some((update) => update.status === "cancelled")).toBeFalse(); + const terminal = socket.updates.at(-1); + expect(terminal?.status === "failed" ? terminal.error.code : undefined).toBe( + "driver.external_tool_effect_unknown", + ); + }); + + test.each(["succeeded", "unknown"] as const)( + "treats a resolved claim as the commit point for a %s provider outcome", + async (outcome) => { + const input = command(`claim-commit-${outcome}`); + const claimStarted = Promise.withResolvers(); + const releaseClaim = Promise.withResolvers(); + class CommitPointSocket extends FakeDriverRuntimeIo { + #reads = 0; + + override async nextCommand(signal: AbortSignal) { + this.#reads += 1; + if (this.#reads === 2) { + await claimStarted.promise; + } + return super.nextCommand(signal); + } + } + const socket = new CommitPointSocket( + [ + input, + { + commandId: `cancel-${input.commandId}`, + kind: "turn.cancel", + reason: "cancel immediately after claim", + runId: DRIVER_TEST_IDS.runId, + }, + ], + DRIVER_TEST_IDS.runId, + ); + const settlements: Parameters[0][] = []; + const settlementSignals: boolean[] = []; + let disposals = 0; + let executions = 0; + + socket.claimExternalToolEffect = async () => { + claimStarted.resolve(); + await releaseClaim.promise; + return { + attempt: 1, + effectId: `test-effect-${input.commandId}`, + idempotencyKey: `test-effect-${input.commandId}`, + kind: "claimed", + }; + }; + socket.settleExternalToolEffect = async (settlement, signal) => { + settlements.push(structuredClone(settlement)); + settlementSignals.push(signal.aborted); + return settlement.settlement.kind === "succeeded" + ? { + effectId: settlement.effectId, + kind: "succeeded", + result: settlement.settlement.result, + } + : { effectId: settlement.effectId, kind: "unknown" }; + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async (_command, prepareSignal) => { + prepareSignal.addEventListener("abort", () => releaseClaim.resolve(), { once: true }); + return { + async execute() { + executions += 1; + expect(prepareSignal.aborted).toBeTrue(); + if (outcome === "unknown") { + throw new Error("provider response lost"); + } + return { + outputText: "committed result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + }, + async [Symbol.asyncDispose]() { + disposals += 1; + }, + }; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + + expect(executions).toBe(1); + expect(disposals).toBe(1); + expect(settlements).toHaveLength(1); + expect(settlements[0]?.settlement.kind).toBe(outcome); + expect(settlementSignals).toEqual([false]); + expect( + socket.updates.some( + (update) => update.commandId === input.commandId && update.status === "cancelled", + ), + ).toBeFalse(); + expect( + socket.updates.findLast( + (update) => update.commandId === input.commandId && update.status !== "accepted", + ), + ).toMatchObject( + outcome === "succeeded" + ? { result: { outputText: "committed result" }, status: "completed" } + : { + error: { code: "driver.external_tool_effect_unknown", retryable: false }, + status: "failed", + }, + ); + }, + ); + + test("uses a fresh settlement budget after cancellation claims the effect", async () => { + const input = command("cancel-after-claim"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const entered = Promise.withResolvers(); + const execution = Promise.withResolvers(); + const shutdown = new AbortController(); + const settlementSignals: boolean[] = []; + socket.settleExternalToolEffect = async (settlement, signal) => { + settlementSignals.push(signal.aborted); + return { effectId: settlement.effectId, kind: "unknown" }; + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => shutdown.signal.aborted, + mcpExecute: async () => { + entered.resolve(); + return execution.promise; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + shutdownSignal: shutdown.signal, + }); + const run = runtime.dispatcher.run(socket, runtime.logger); + + await entered.promise; + shutdown.abort(new Error("test shutdown")); + execution.reject(new Error("provider response lost")); + await run; + + expect(settlementSignals).toEqual([false]); + expect(socket.updates.some((update) => update.status === "cancelled")).toBeFalse(); + const terminal = socket.updates.at(-1); + expect(terminal?.status === "failed" ? terminal.error.code : undefined).toBe( + "driver.external_tool_effect_unknown", + ); + }); + + test("retries the identical settlement and trusts its stored result", async () => { + const input = command("settlement-retry"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const settlements: Parameters[0][] = []; + const storedResult = { + outputText: "stored canonical result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + socket.settleExternalToolEffect = async (settlement) => { + settlements.push(structuredClone(settlement)); + if (settlements.length === 1) { + throw new Error("settlement acknowledgement lost"); + } + return { effectId: settlement.effectId, kind: "succeeded", result: storedResult }; + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpExecute: async () => ({ + outputText: "provider result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate(socket, (update) => update.status === "completed"); + + expect(settlements).toHaveLength(2); + expect(settlements[1]).toEqual(settlements[0]); + const terminal = socket.updates.at(-1); + expect(terminal?.status === "completed" ? terminal.result : undefined).toEqual(storedResult); + }); + + test("leaves a successfully invoked effect accepted when settlement is unreachable", async () => { + const input = command("settlement-unreachable"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + let executions = 0; + let settlements = 0; + socket.settleExternalToolEffect = async () => { + settlements += 1; + throw new Error("settlement unreachable"); + }; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpExecute: async () => { + executions += 1; + return { + outputText: "provider result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const run = runtime.dispatcher.run(socket, runtime.logger); + await waitFor(() => runtime.shutdownCalls.includes("driver.mcp_task_failed")); + await run.catch(() => {}); + + expect(executions).toBe(1); + expect(settlements).toBe(2); + expect(socket.updates).toEqual([{ commandId: input.commandId, status: "accepted" }]); + }); + + test("treats MCP cleanup failure as diagnostic-only", async () => { + const input = command("cleanup-diagnostic"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => ({ + async execute() { + return { + outputText: "durable result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + }, + async [Symbol.asyncDispose]() { + throw new Error("cleanup failed"); + }, + }), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate(socket, (update) => update.status === "completed"); + + expect(socket.updates.at(-1)).toMatchObject({ + commandId: input.commandId, + status: "completed", + }); + expect(runtime.shutdownCalls).toEqual([]); + }); +}); diff --git a/tests/driver-golden-fixtures.test.ts b/tests/driver-golden-fixtures.test.ts index 1a36b45..da57768 100644 --- a/tests/driver-golden-fixtures.test.ts +++ b/tests/driver-golden-fixtures.test.ts @@ -1,8 +1,17 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; +import { DriverCommandDelivery } from "../src/core/driver-command-delivery"; import type { EventId } from "../src/protocol/id"; -import { parseRuntimeCommand } from "../src/runtime-command"; +import { parseDriverEventEnvelope } from "../src/protocol/events"; +import { + DURABLE_RUN_ERROR_MAX_UTF8_BYTES, + RUNTIME_COMMAND_MAX_UTF8_BYTES, + measureRuntimeCommandJson, + normalizeDurableRunError, + parseRuntimeCommand, +} from "../src/runtime-command"; +import type { RuntimeCommandInput } from "../src/runtime-command"; import { ingestRuntimeEventInput } from "../src/runtime-events"; import type { RuntimeEventBuildContext } from "../src/runtime-events"; import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; @@ -24,6 +33,7 @@ const runtimeEventFixtures = [ "message-delta", "permission-requested", "run-started", + "tool-call-deltas", "tool-call-updated", "usage-updated", ] as const; @@ -32,6 +42,39 @@ function readJsonFixture(path: string): unknown { return JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")); } +function textFieldAtJsonSize( + targetBytes: number, + create: (text: string) => Value, + unit = "x", +): Value { + const baseBytes = measureRuntimeCommandJson(create("")); + const unitBytes = measureRuntimeCommandJson(create(unit)) - baseBytes; + const remaining = targetBytes - baseBytes; + const value = create( + unit.repeat(Math.floor(remaining / unitBytes)) + "x".repeat(remaining % unitBytes), + ); + + expect(measureRuntimeCommandJson(value)).toBe(targetBytes); + return value; +} + +function mcpCommandAtSize(targetBytes: number, unit = "x") { + return textFieldAtJsonSize( + targetBytes, + (argumentsJson) => ({ + argumentsJson, + commandId: "command-1", + kind: "mcp.execute" as const, + requestId: "request-1", + runId: DRIVER_TEST_IDS.runId, + serverId: "server-1", + toolCallId: "tool-call-1", + toolName: "tool-1", + }), + unit, + ); +} + function createRuntimeEventContext(): RuntimeEventBuildContext { return { createId: () => eventId, @@ -50,17 +93,34 @@ describe("Driver golden fixtures", () => { expect(parseRuntimeCommand(fixture)).toEqual(fixture); }); - test("preserves v1 compatibility for API-materialized input attachments", () => { + test("preserves validated attachment provenance on the exported command input", () => { + const fixture = readJsonFixture("./fixtures/driver/commands/input-start.json") as { + readonly input: { readonly text: string }; + }; + const attachmentIds = ["file-1"]; + const input = { + ...fixture.input, + attachmentIds, + } satisfies RuntimeCommandInput; + const parsed = parseRuntimeCommand({ ...fixture, input }); + attachmentIds[0] = "mutated"; + + expect(parsed).toEqual({ ...fixture, input: { ...input, attachmentIds: ["file-1"] } }); + }); + + test("keeps attachment provenance in replay identity after provider projection", () => { const fixture = readJsonFixture("./fixtures/driver/commands/input-start.json") as { readonly input: Record; }; + const first = { ...fixture, input: { ...fixture.input, attachmentIds: ["file-1"] } }; + const changed = { ...fixture, input: { ...fixture.input, attachmentIds: ["file-2"] } }; + const delivery = new DriverCommandDelivery(new AbortController().signal); - expect( - parseRuntimeCommand({ - ...fixture, - input: { ...fixture.input, attachmentIds: ["file-1"] }, - }), - ).toEqual(fixture); + delivery.receive(parseRuntimeCommand(first), first); + + expect(() => delivery.replay(parseRuntimeCommand(changed), changed)).toThrow( + "replayed with changed identity or content", + ); }); test("rejects malformed input attachment IDs", () => { @@ -76,6 +136,37 @@ describe("Driver golden fixtures", () => { ).toThrow("input.attachmentIds must be an array of non-empty strings."); }); + test.each(commandFixtures)("rejects undeclared fields on runtime command %s", (name) => { + const fixture = readJsonFixture(`./fixtures/driver/commands/${name}.json`); + + expect(() => parseRuntimeCommand({ ...(fixture as object), extra: true })).toThrow( + "runtime command.extra is not allowed.", + ); + }); + + test("rejects undeclared fields on runtime command input", () => { + const fixture = readJsonFixture("./fixtures/driver/commands/input-start.json") as { + readonly input: Record; + }; + + expect(() => + parseRuntimeCommand({ ...fixture, input: { ...fixture.input, extra: true } }), + ).toThrow("input.extra is not allowed."); + }); + + test.each(["input-start", "mcp-execute", "permission-resolve", "turn-cancel"] as const)( + "requires runId on run-scoped runtime command %s", + (name) => { + const fixture = readJsonFixture(`./fixtures/driver/commands/${name}.json`) as Record< + string, + unknown + >; + const { runId: _runId, ...withoutRunId } = fixture; + + expect(() => parseRuntimeCommand(withoutRunId)).toThrow("runId must be a non-empty string."); + }, + ); + test.each(runtimeEventFixtures)("ingests runtime event fixture %s", (name) => { const outcome = ingestRuntimeEventInput( createRuntimeEventContext(), @@ -95,6 +186,104 @@ describe("Driver golden fixtures", () => { ); }); + test("rejects extensions on fixed event envelope layers while preserving payload extensions", () => { + const context = createRuntimeEventContext(); + const fixture = readJsonFixture( + "./fixtures/driver/runtime-event-envelopes/diagnostic-reported.json", + ) as Record; + + expect(ingestRuntimeEventInput(context, { ...fixture, context: {} })).toMatchObject({ + rejection: { message: "Runtime event context is not allowed." }, + status: "rejected", + }); + expect( + ingestRuntimeEventInput(context, { + ...fixture, + native: { future: true, provider: "openai" }, + }), + ).toMatchObject({ + rejection: { message: "Runtime event native reference future is not allowed." }, + status: "rejected", + }); + expect(() => + parseDriverEventEnvelope({ event: fixture, eventId: "event-1", sequence: 1 }), + ).toThrow("Driver event envelope sequence is not allowed."); + + const accepted = ingestRuntimeEventInput(context, { + kind: "diagnostic.reported", + payload: { code: "driver.test", future: { nested: true } }, + }); + expect(accepted).toMatchObject({ + event: { payload: { code: "driver.test", future: { nested: true } } }, + status: "accepted", + }); + if (accepted.status === "accepted") { + expect(ingestRuntimeEventInput(context, accepted.event)).toEqual(accepted); + } + }); + + test("owns tool call snapshot and delta payload semantics", () => { + const context = createRuntimeEventContext(); + const event = { + kind: "tool.call.updated" as const, + payload: { + future: { nested: true }, + rawInputDelta: '{"path":', + rawOutputDelta: "chunk", + status: "running", + toolCallId: "tool-delta-1", + }, + }; + + expect(ingestRuntimeEventInput(context, event)).toMatchObject({ + event: { payload: event.payload }, + status: "accepted", + }); + expect( + ingestRuntimeEventInput(context, { + ...event, + payload: { ...event.payload, rawInput: '{"path":"src"}' }, + }), + ).toMatchObject({ + rejection: { + message: + "Runtime event tool.call.updated payload cannot contain both rawInput and rawInputDelta.", + }, + status: "rejected", + }); + expect( + ingestRuntimeEventInput(context, { + ...event, + payload: { ...event.payload, rawOutput: "complete" }, + }), + ).toMatchObject({ + rejection: { + message: + "Runtime event tool.call.updated payload cannot contain both rawOutput and rawOutputDelta.", + }, + status: "rejected", + }); + + for (const payload of [ + { ...event.payload, rawInputDelta: 1 }, + { ...event.payload, rawOutputDelta: null }, + ]) { + expect(ingestRuntimeEventInput(context, { ...event, payload })).toMatchObject({ + status: "rejected", + }); + } + + expect( + ingestRuntimeEventInput(context, { + ...event, + payload: { ...event.payload, rawInputMode: "future-extension" }, + }), + ).toMatchObject({ + event: { payload: { ...event.payload, rawInputMode: "future-extension" } }, + status: "accepted", + }); + }); + test.each([Number.NaN, Number.POSITIVE_INFINITY, undefined])( "rejects non-JSON usage payload value %p at the shared event boundary", (tokens) => { @@ -179,23 +368,242 @@ describe("Driver golden fixtures", () => { ).toMatchObject({ status: "rejected" }); }); - test("bounds control reasons without dropping their commands", () => { - const limit = "x".repeat(16 * 1_024); - const oversized = `${limit}界`; - const summary = "Runtime command reason exceeded 16384 UTF-8 bytes (received 16387)."; + test("owns the completed-run final message reference schema", () => { + const context = createRuntimeEventContext(); + for (const payload of [{ finalMessageId: "message-1" }, {}]) { + expect(ingestRuntimeEventInput(context, { kind: "run.completed", payload })).toMatchObject({ + status: "accepted", + }); + } + + for (const payload of [ + { finalMessageId: "" }, + { finalMessageId: null }, + { finalMessageId: 1 }, + { finalMessageId: "message-1", finalMessageText: "answer" }, + { finalMessageText: "answer" }, + ]) { + expect(ingestRuntimeEventInput(context, { kind: "run.completed", payload })).toMatchObject({ + status: "rejected", + }); + } + }); + + test.each([ + ["run.cancel.requested", "running", "completed"], + ["run.cancelled", "cancelled", "completed"], + ["run.completed", "completed", "failed"], + ["run.dispatched", "booting", "completed"], + ["run.failed", "failed", "completed"], + ["run.queued", "queued", "running"], + ["run.started", "running", "completed"], + ["run.steered", "waiting_input", "completed"], + ["run.waiting", "waiting_input", "completed"], + ] as const)("requires %s to agree with payload run.status", (kind, status, inconsistent) => { + const error = { code: "test.failed", details: {}, message: "failed", retryable: false }; + const payload = { + ...(kind === "run.failed" ? { error, recoverable: false } : {}), + run: { + completedAt: null, + error: kind === "run.failed" ? error : null, + startedAt: occurredAt, + status, + }, + }; + + expect(ingestRuntimeEventInput(createRuntimeEventContext(), { kind, payload })).toMatchObject({ + status: "accepted", + }); + expect( + ingestRuntimeEventInput(createRuntimeEventContext(), { + kind, + payload: { ...payload, run: { ...payload.run, status: inconsistent } }, + }), + ).toMatchObject({ + rejection: { message: `Runtime event ${kind} payload run.status is inconsistent.` }, + status: "rejected", + }); + }); + + test.each([ + ["run.cancel.requested", "running", "completed"], + ["run.cancelled", "cancelled", "completed"], + ["run.completed", "completed", "failed"], + ["run.dispatched", "booting", "completed"], + ["run.failed", "failed", "completed"], + ["run.queued", "queued", "running"], + ["run.started", "running", "completed"], + ["run.steered", "waiting_input", "completed"], + ["run.waiting", "waiting_input", "completed"], + ] as const)("requires %s to agree with payload status", (kind, status, inconsistent) => { + const error = { code: "test.failed", details: {}, message: "failed", retryable: false }; + const payload = { + ...(kind === "run.failed" ? { error, recoverable: false } : {}), + ...(kind === "run.started" ? { startedAt: occurredAt } : {}), + status, + }; + + expect(ingestRuntimeEventInput(createRuntimeEventContext(), { kind, payload })).toMatchObject({ + status: "accepted", + }); + expect( + ingestRuntimeEventInput(createRuntimeEventContext(), { + kind, + payload: { ...payload, status: inconsistent }, + }), + ).toMatchObject({ + rejection: { message: `Runtime event ${kind} payload status is inconsistent.` }, + status: "rejected", + }); + }); + + test("requires failed-run recoverability to agree with the durable error", () => { + const event = { + kind: "run.failed" as const, + payload: { + error: { + code: "driver.retryable", + details: {}, + message: "retryable", + retryable: true, + }, + recoverable: true, + status: "failed", + }, + }; + + expect(ingestRuntimeEventInput(createRuntimeEventContext(), event)).toMatchObject({ + status: "accepted", + }); + expect( + ingestRuntimeEventInput(createRuntimeEventContext(), { + ...event, + payload: { ...event.payload, recoverable: false }, + }), + ).toMatchObject({ + rejection: { + message: "Runtime event run.failed payload recoverable must agree with error.retryable.", + }, + status: "rejected", + }); expect( - parseRuntimeCommand({ commandId: "cancel-at-limit", kind: "turn.cancel", reason: limit }), - ).toMatchObject({ reason: limit }); + ingestRuntimeEventInput(createRuntimeEventContext(), { + ...event, + payload: { ...event.payload, recoverable: "yes" }, + }), + ).toMatchObject({ status: "rejected" }); + expect( + ingestRuntimeEventInput(createRuntimeEventContext(), { + ...event, + payload: { ...event.payload, recoverable: undefined }, + }), + ).toMatchObject({ status: "rejected" }); + expect( + ingestRuntimeEventInput(createRuntimeEventContext(), { + ...event, + payload: { + ...event.payload, + error: { ...event.payload.error, retryable: undefined }, + }, + }), + ).toMatchObject({ status: "rejected" }); + }); + + test("rejects non-primitive durable run error details instead of filtering them", () => { + expect( + ingestRuntimeEventInput(createRuntimeEventContext(), { + kind: "run.failed", + payload: { + error: { + code: "driver.invalid_details", + details: { nested: { value: "lost" } }, + message: "invalid details", + retryable: false, + }, + recoverable: false, + status: "failed", + }, + }), + ).toMatchObject({ + rejection: { + message: + "Runtime event run.failed payload error.details.nested must be a primitive JSON value.", + }, + status: "rejected", + }); + }); + + test("preserves control reasons within the durable command limit", () => { + const reason = `${"x".repeat(16 * 1_024)}界`; + expect( parseRuntimeCommand({ - commandId: "cancel-oversized", + commandId: "cancel", kind: "turn.cancel", - reason: oversized, + reason, + runId: DRIVER_TEST_IDS.runId, }), - ).toMatchObject({ kind: "turn.cancel", reason: summary }); - expect( - parseRuntimeCommand({ commandId: "stop-oversized", kind: "session.stop", reason: oversized }), - ).toMatchObject({ kind: "session.stop", reason: summary }); + ).toMatchObject({ kind: "turn.cancel", reason }); + expect(parseRuntimeCommand({ commandId: "stop", kind: "session.stop", reason })).toMatchObject({ + kind: "session.stop", + reason, + }); + }); + + test.each(["x", "界", "\0"])( + "bounds canonical runtime command JSON after %p UTF-8 encoding", + (unit) => { + const exact = mcpCommandAtSize(RUNTIME_COMMAND_MAX_UTF8_BYTES, unit); + + expect(parseRuntimeCommand(exact)).toEqual(exact); + expect(() => + parseRuntimeCommand({ ...exact, argumentsJson: `${exact.argumentsJson}x` }), + ).toThrow(`Runtime command exceeds ${String(RUNTIME_COMMAND_MAX_UTF8_BYTES)} UTF-8 bytes.`); + }, + ); + + test.each(["x", "界", "\0"])( + "omits an oversized command error after %p UTF-8 encoding", + (unit) => { + const exact = textFieldAtJsonSize( + DURABLE_RUN_ERROR_MAX_UTF8_BYTES, + (message) => ({ code: "driver.failed", details: {}, message, retryable: false }), + unit, + ); + const oversized = { ...exact, message: `${exact.message}x` }; + + expect(normalizeDurableRunError(exact)).toBe(exact); + expect(normalizeDurableRunError(oversized)).toEqual({ + code: "driver.error_oversized", + details: { originalBytes: DURABLE_RUN_ERROR_MAX_UTF8_BYTES + 1 }, + message: `Driver error exceeded ${String(DURABLE_RUN_ERROR_MAX_UTF8_BYTES)} UTF-8 bytes and was omitted.`, + retryable: false, + }); + }, + ); +}); + +describe("Driver runtime stream identity contract", () => { + test.each([ + ["message.added", "messageId", { content: "message" }], + ["message.cancelled", "messageId", {}], + ["message.completed", "messageId", {}], + ["message.delta", "messageId", { contentDelta: "message" }], + [ + "message.failed", + "messageId", + { error: { code: "failed", message: "failed", retryable: false } }, + ], + ["message.started", "messageId", {}], + ["thought.cancelled", "thoughtId", {}], + ["thought.completed", "thoughtId", {}], + ["thought.delta", "thoughtId", { contentDelta: "thought" }], + ["thought.started", "thoughtId", {}], + ] as const)("rejects %s without %s", (kind, identity, payload) => { + expect(ingestRuntimeEventInput(createRuntimeEventContext(), { kind, payload })).toMatchObject({ + rejection: { message: `${kind} ${identity} must be a non-empty string.` }, + status: "rejected", + }); }); }); diff --git a/tests/driver-instance-socket-process.test.ts b/tests/driver-instance-socket-process.test.ts index 20be177..e87aca1 100644 --- a/tests/driver-instance-socket-process.test.ts +++ b/tests/driver-instance-socket-process.test.ts @@ -7,7 +7,7 @@ import { DriverTurnCancelledError } from "../src/core/driver-runtime-state"; import type { RuntimeCommand } from "../src/runtime-command"; import { settlePromiseWithTimeout } from "../src/utils/async"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; -import { createBackend } from "./driver-runtime-boundary-fixtures"; +import { createBackend, settleBackendInput } from "./driver-runtime-boundary-fixtures"; const nativeWebSocket = globalThis.WebSocket; const nativeAbortSignalTimeout = AbortSignal.timeout; @@ -138,12 +138,17 @@ class RpcWebSocket extends OpenWebSocket { runId: null, }; } else if (path === "/driver/pushEvents") { - const events = (input as { events: { event: { kind: string } }[] }).events; + const events = ( + input as { + events: { event: { id: string; kind: string; sourceEventId?: string } }[]; + } + ).events; this.eventBatchSizes.push(events.length); const acceptedCount = RpcWebSocket.acceptedEventCounts.shift() ?? events.length; const accepted = events.slice(0, acceptedCount).map(({ event }) => { this.#nextEventSeq += 1; return { + eventId: event.sourceEventId ?? event.id, seq: this.#nextEventSeq, type: event.kind, }; @@ -331,6 +336,60 @@ describe("DriverProcess lifecycle", () => { expect(ready).toBeLessThan(heartbeat); }); + test("keeps the RPC lane open until backend-owned file reporting drains", async () => { + globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; + RpcWebSocket.stalledPath = "/driverInstance/nextCommand"; + RpcWebSocket.delayedEventKind = "file.changed"; + const existingSignalListeners = new Set(process.listeners("SIGTERM")); + const stopEntered = Promise.withResolvers(); + const backend = createBackend(); + let context: AgentDriverContext | null = null; + let report: Promise | null = null; + backend.start = async (startedContext) => { + context = startedContext; + }; + backend.stop = async () => { + stopEntered.resolve(); + await report; + }; + const run = new DriverProcess(driverBootPayload, () => backend).run(); + + await RpcWebSocket.stalled.promise; + RpcWebSocket.stalled = Promise.withResolvers(); + report = (context as AgentDriverContext | null)!.ports.file.reportChanged( + { + change: "upsert", + path: "/workspace/committed.txt", + reason: "test.commit", + }, + AbortSignal.timeout(5_000), + ); + void report.catch(() => {}); + await RpcWebSocket.stalled.promise; + + const shutdown = process + .listeners("SIGTERM") + .find((listener) => !existingSignalListeners.has(listener)); + expect(shutdown).toBeDefined(); + shutdown?.("SIGTERM"); + await stopEntered.promise; + expect((PendingWebSocket.instances[0] as RpcWebSocket).readyState).toBe(1); + + RpcWebSocket.delayedResponse.resolve(); + await expect(run).resolves.toBeUndefined(); + const fileEvents = (PendingWebSocket.instances[0] as RpcWebSocket).requests.flatMap( + ({ input, path }) => + path === "/driver/pushEvents" + ? ( + input as { + events: { event: { kind: string } }[]; + } + ).events.filter(({ event }) => event.kind === "file.changed") + : [], + ); + expect(fileEvents).toHaveLength(1); + }); + test("fails after an unexpected control disconnect and still cleans the backend", async () => { globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; RpcWebSocket.stalledPath = "/driverInstance/nextCommand"; @@ -392,6 +451,35 @@ describe("DriverProcess lifecycle", () => { expect(paths.filter((path) => path === "/driver/failRun")).toHaveLength(2); }); + test("fails the exact second run when its provider exits before a run event", async () => { + globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; + RpcWebSocket.commands = [ + { + commandId: "second-run-provider-failure", + input: { text: "fail before terminal" }, + kind: "input.start", + requestId: "second-run-provider-failure-request", + runId: DRIVER_TEST_IDS.secondRunId, + }, + ]; + const backend = createBackend(); + backend.handleInput = async () => { + throw new Error("second run provider exited"); + }; + + await expect(new DriverProcess(driverBootPayload, () => backend).run()).rejects.toThrow( + "second run provider exited", + ); + + const failure = (PendingWebSocket.instances[0] as RpcWebSocket).requests.find( + ({ path }) => path === "/driver/failRun", + ); + expect(failure?.input).toMatchObject({ runId: DRIVER_TEST_IDS.secondRunId }); + expect(failure?.input).not.toMatchObject({ + runId: driverBootPayload.execution.configRevision.runId, + }); + }); + test("does not report a control terminal when backend cleanup remains failed", async () => { globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; RpcWebSocket.stalledPath = "/driverInstance/nextCommand"; @@ -432,7 +520,7 @@ describe("DriverProcess lifecycle", () => { let decision: "allow_once" | "reject_once" | null = null; let sideEffect = false; let stoppedBeforePermissionSettled = false; - backend.handleInput = async (context) => { + backend.handleInput = async (context, _input, runId, signal) => { decision = await context.ports.permission.request({ rawInput: '{"command":"touch should-not-exist"}', requestId: "pending-permission", @@ -446,19 +534,7 @@ describe("DriverProcess lifecycle", () => { return; } - await context.ports.eventSink.pushEvents({ - events: [ - { - kind: "run.cancelled", - payload: { - reason: "signal.sigterm", - requestedBy: "user", - stopReason: "cancelled", - }, - runId: DRIVER_TEST_IDS.runId, - }, - ], - }); + await settleBackendInput(context, runId, signal); }; backend.stop = async () => { stoppedBeforePermissionSettled = decision === null; @@ -554,6 +630,7 @@ describe("DriverProcess lifecycle", () => { commandId: `terminal-${selectionOrder}-cancel`, kind: "turn.cancel", reason: "test cancellation", + runId: DRIVER_TEST_IDS.runId, }, { commandId: `terminal-${selectionOrder}-stop`, @@ -565,20 +642,12 @@ describe("DriverProcess lifecycle", () => { const cancellationEntered = Promise.withResolvers(); const allowTerminalSelection = Promise.withResolvers(); const backend = createBackend(); - backend.handleInput = async (context, _input, runId) => { + backend.handleInput = async (context, _input, runId, signal) => { inputEntered.resolve(); if (selectionOrder === "after") { await allowTerminalSelection.promise; } - await context.ports.eventSink.pushEvents({ - events: [ - { - kind: "run.completed", - payload: { stopReason: "end_turn" }, - runId, - }, - ], - }); + await settleBackendInput(context, runId, signal); }; backend.cancelActiveTurn = async () => { cancellationEntered.resolve(); @@ -592,9 +661,10 @@ describe("DriverProcess lifecycle", () => { await inputEntered.promise; if (selectionOrder === "before") { await RpcWebSocket.stalled.promise; + RpcWebSocket.delayedResponse.resolve(); + } else { + await cancellationEntered.promise; } - await cancellationEntered.promise; - RpcWebSocket.delayedResponse.resolve(); await expect(run).resolves.toBeUndefined(); const inputTerminals = (PendingWebSocket.instances[0] as RpcWebSocket).requests @@ -870,10 +940,10 @@ describe("DriverProcess lifecycle", () => { const inputEntered = Promise.withResolvers(); const stopInput = Promise.withResolvers(); const backend = createBackend(); - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId, signal) => { inputEntered.resolve(); await stopInput.promise; - throw new DriverTurnCancelledError("signal shutdown"); + await settleBackendInput(context, runId, signal); }; backend.stop = async () => stopInput.resolve(); const run = new DriverProcess(driverBootPayload, () => backend).run(); diff --git a/tests/driver-instance-socket-socket.test.ts b/tests/driver-instance-socket-socket.test.ts index 60a865b..854bdb2 100644 --- a/tests/driver-instance-socket-socket.test.ts +++ b/tests/driver-instance-socket-socket.test.ts @@ -2,6 +2,10 @@ import { afterEach, describe, expect, test } from "bun:test"; import { DriverInstanceSocket } from "../src/infrastructure/runtime/driver-instance-socket"; import type { DriverEventInput } from "../src/protocol/events"; +import { + DURABLE_RUN_ERROR_MAX_UTF8_BYTES, + measureRuntimeCommandJson, +} from "../src/runtime-command"; import { settlePromiseWithTimeout } from "../src/utils/async"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; @@ -120,12 +124,17 @@ class RpcWebSocket extends OpenWebSocket { runId: null, }; } else if (path === "/driver/pushEvents") { - const events = (input as { events: { event: { kind: string } }[] }).events; + const events = ( + input as { + events: { event: { id: string; kind: string; sourceEventId?: string } }[]; + } + ).events; this.eventBatchSizes.push(events.length); const acceptedCount = RpcWebSocket.acceptedEventCounts.shift() ?? events.length; const accepted = events.slice(0, acceptedCount).map(({ event }) => { this.#nextEventSeq += 1; return { + eventId: event.sourceEventId ?? event.id, seq: this.#nextEventSeq, type: event.kind, }; @@ -438,28 +447,32 @@ describe("DriverInstanceSocket lifecycle", () => { const socket = await connectRpcSocket(); socket.abortPendingRequests("test shutdown"); const signal = new AbortController().signal; + const result = { + outputText: "done", + requestId: "effect-request", + serverId: "effect-server", + toolName: "lookup", + }; + RpcWebSocket.responseOverrides.set("/driver/settleExternalToolEffect", { + effectId: "effect-1", + kind: "succeeded", + result, + }); await expect( - socket.completeExternalToolEffect( + socket.settleExternalToolEffect( { + claimToken: "00000000-0000-4000-8000-000000000001", commandId: "effect-command", - result: { - outputText: "done", - requestId: "effect-request", - serverId: "effect-server", - toolName: "lookup", - }, + effectId: "effect-1", + settlement: { kind: "succeeded", result }, }, signal, ), - ).resolves.toBeUndefined(); - await expect( - socket.markExternalToolEffectUnknown({ commandId: "unknown-command" }, signal), - ).resolves.toBeUndefined(); + ).resolves.toEqual({ effectId: "effect-1", kind: "succeeded", result }); expect((PendingWebSocket.instances[0] as RpcWebSocket).paths).toEqual([ - "/driver/completeExternalToolEffect", - "/driver/markExternalToolEffectUnknown", + "/driver/settleExternalToolEffect", ]); }); @@ -482,10 +495,16 @@ describe("DriverInstanceSocket lifecycle", () => { attempt: 0, effectId: "effect-1", idempotencyKey: "effect-1", - kind: "execute", + kind: "claimed", }); await expect( - socket.claimExternalToolEffect({ commandId: "command-1" }, signal), + socket.claimExternalToolEffect( + { + claimToken: "00000000-0000-4000-8000-000000000001", + commandId: "command-1", + }, + signal, + ), ).rejects.toThrow("attempt must be a positive safe integer"); }); @@ -505,6 +524,37 @@ describe("DriverInstanceSocket lifecycle", () => { ).toHaveLength(2); }); + test("targets the active second run after the first run is released", async () => { + const socket = await connectRpcSocket(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + const first = socket.beginRun(DRIVER_TEST_IDS.runId); + await socket.pushEvents({ + events: [{ kind: "run.completed", payload: { stopReason: "end_turn" } }], + }); + socket.releaseRun(first, "command_acked"); + socket.beginRun(DRIVER_TEST_IDS.secondRunId); + + await socket.failRun({ + code: "second.failed", + details: {}, + message: "second failed", + retryable: false, + }); + + expect( + ( + (PendingWebSocket.instances[0] as RpcWebSocket).requests.find( + ({ path }) => path === "/driver/failRun", + )!.input as { runId: string } + ).runId, + ).toBe(DRIVER_TEST_IDS.secondRunId); + }); + test("uses the terminal-attempt signal to abort only that command update", async () => { globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; RpcWebSocket.stalledPath = "/driver/commandUpdate"; @@ -538,28 +588,31 @@ describe("DriverInstanceSocket lifecycle", () => { test.each([ ["completeRun", "/driver/completeRun", "/driver/failRun"], ["failRun", "/driver/failRun", "/driver/completeRun"], - ] as const)("keeps a claimed %s run terminal monotonic", async (first, sent, skipped) => { - const socket = await connectRpcSocket(); - socket.beginRun(DRIVER_TEST_IDS.runId); - const failure = { - code: "test.failure", - details: {}, - message: "failed", - retryable: false, - }; + ] as const)( + "rejects a conflicting terminal after %s is acknowledged", + async (first, sent, skipped) => { + const socket = await connectRpcSocket(); + socket.beginRun(DRIVER_TEST_IDS.runId); + const failure = { + code: "test.failure", + details: {}, + message: "failed", + retryable: false, + }; - if (first === "completeRun") { - await socket.completeRun(); - await socket.failRun(failure); - } else { - await socket.failRun(failure); - await socket.completeRun(); - } + if (first === "completeRun") { + await socket.completeRun(); + await expect(socket.failRun(failure)).rejects.toThrow("conflicts"); + } else { + await socket.failRun(failure); + await expect(socket.completeRun()).rejects.toThrow("conflicts"); + } - const paths = (PendingWebSocket.instances[0] as RpcWebSocket).paths; - expect(paths.filter((path) => path === sent)).toHaveLength(1); - expect(paths).not.toContain(skipped); - }); + const paths = (PendingWebSocket.instances[0] as RpcWebSocket).paths; + expect(paths.filter((path) => path === sent)).toHaveLength(1); + expect(paths).not.toContain(skipped); + }, + ); test.each([ ["completeRun", "/driver/completeRun", "/driver/failRun"], @@ -582,7 +635,7 @@ describe("DriverInstanceSocket lifecycle", () => { RpcWebSocket.sendFailurePath = sent; await expect(deliver()).rejects.toThrow("test wire send failed"); - await expect(deliverOpposite()).resolves.toBeUndefined(); + await expect(deliverOpposite()).rejects.toThrow("conflicts"); await expect(deliver()).resolves.toBeUndefined(); await expect(deliver()).resolves.toBeUndefined(); @@ -645,7 +698,7 @@ describe("DriverInstanceSocket lifecycle", () => { failure.message = "mutated failure"; await expect(first).rejects.toThrow("test wire send failed"); - await expect(socket.failRun(failure)).rejects.toThrow("different error"); + await expect(socket.failRun(failure)).rejects.toThrow("conflicts"); await expect(socket.failRun(selected)).resolves.toBeUndefined(); await expect(socket.failRun(selected)).resolves.toBeUndefined(); @@ -655,11 +708,46 @@ describe("DriverInstanceSocket lifecycle", () => { input: { driverInstanceId: DRIVER_TEST_IDS.driverInstanceId, error: selected, + runId: DRIVER_TEST_IDS.runId, }, path: "/driver/failRun", }); }); + test.each([ + ["command update", "/driver/commandUpdate"], + ["run terminal", "/driver/failRun"], + ] as const)("omits an oversized %s error before RPC delivery", async (kind, path) => { + const socket = await connectRpcSocket(); + const base = { code: "test.failure", details: {}, message: "", retryable: false }; + const oversized = { + ...base, + message: "x".repeat(DURABLE_RUN_ERROR_MAX_UTF8_BYTES + 1 - measureRuntimeCommandJson(base)), + }; + + if (kind === "command update") { + await socket.commandUpdate( + { commandId: "oversized-error-command", error: oversized, status: "failed" }, + new AbortController().signal, + ); + } else { + socket.beginRun(DRIVER_TEST_IDS.runId); + await socket.failRun(oversized); + } + + expect(measureRuntimeCommandJson(oversized)).toBe(DURABLE_RUN_ERROR_MAX_UTF8_BYTES + 1); + const request = (PendingWebSocket.instances[0] as RpcWebSocket).requests.find( + (candidate) => candidate.path === path, + ); + expect(request).toBeDefined(); + expect((request!.input as { error: unknown }).error).toEqual({ + code: "driver.error_oversized", + details: { originalBytes: DURABLE_RUN_ERROR_MAX_UTF8_BYTES + 1 }, + message: `Driver error exceeded ${String(DURABLE_RUN_ERROR_MAX_UTF8_BYTES)} UTF-8 bytes and was omitted.`, + retryable: false, + }); + }); + test.each([ ["completeRun", "/driver/completeRun"], ["failRun", "/driver/failRun"], @@ -692,6 +780,59 @@ describe("DriverInstanceSocket lifecycle", () => { const secondWire = PendingWebSocket.instances[1] as RpcWebSocket; expect(firstWire.paths.filter((sent) => sent === path)).toHaveLength(1); expect(secondWire.paths.filter((sent) => sent === path)).toHaveLength(1); + expect( + [firstWire, secondWire].map( + (wire) => + (wire.requests.find((request) => request.path === path)!.input as { runId: string }) + .runId, + ), + ).toEqual([DRIVER_TEST_IDS.runId, DRIVER_TEST_IDS.runId]); + }); + + test("reuses an implicit run terminal identity after its persisted ACK is lost", async () => { + globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; + const socket = new DriverInstanceSocket(driverBootPayload, { onClose: () => {} }); + await socket.connect(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + socket.beginRun(DRIVER_TEST_IDS.runId); + const event: DriverEventInput = { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + }; + const firstWire = PendingWebSocket.instances[0] as RpcWebSocket; + RpcWebSocket.lostResponsePath = "/driver/pushEvents"; + + const first = socket.pushEvents({ events: [event] }); + await RpcWebSocket.stalled.promise; + firstWire.close(1006, "terminal ACK lost"); + await expect(first).rejects.toThrow("terminal ACK lost"); + + await socket.connect(); + await socket.hello({ + capabilities: [], + driverVersion: "test", + protocolVersion: driverBootPayload.protocolVersion, + startedAt: new Date(0).toISOString(), + }); + await expect(socket.pushEvents({ events: [event] })).resolves.toMatchObject({ + accepted: [{ type: "run.completed" }], + }); + + const sourceIds = [firstWire, PendingWebSocket.instances[1] as RpcWebSocket].map( + (wire) => + ( + wire.requests.find((request) => request.path === "/driver/pushEvents")!.input as { + events: { event: { sourceEventId: string } }[]; + } + ).events[0]!.event.sourceEventId, + ); + expect(sourceIds[0]).toBeString(); + expect(sourceIds[1]).toBe(sourceIds[0]); }); test("rejects event receipts that are not a submitted-prefix", async () => { @@ -704,8 +845,8 @@ describe("DriverInstanceSocket lifecycle", () => { }); RpcWebSocket.responseOverrides.set("/driver/pushEvents", { accepted: [ - { seq: 1, type: "message.completed" }, - { seq: 2, type: "message.completed" }, + { eventId: "extra-1", seq: 1, type: "message.completed" }, + { eventId: "extra-2", seq: 2, type: "message.completed" }, ], }); @@ -781,7 +922,7 @@ describe("DriverInstanceSocket lifecycle", () => { expect(result.accepted).toHaveLength(3); }); - test("linearizes each run at one fully acknowledged final terminal", async () => { + test("isolates each run terminal before reserving or delivering it", async () => { RpcWebSocket.eventBatchMaxSize = 2; const socket = await connectRpcSocket(); await socket.hello({ @@ -798,10 +939,15 @@ describe("DriverInstanceSocket lifecycle", () => { const completed: DriverEventInput = { kind: "run.completed", payload: { stopReason: "end_turn" }, + sourceEventId: "linearized-run-completed", }; + let barrierCalls = 0; + socket.registerRunTerminalBarrier(() => { + barrierCalls += 1; + }); await expect(socket.pushEvents({ events: [completed, delta] })).rejects.toThrow( - "must be the final event", + "must be the only event", ); await expect( socket.pushEvents({ @@ -809,7 +955,10 @@ describe("DriverInstanceSocket lifecycle", () => { completed, { kind: "run.failed", - payload: { error: { code: "failed", message: "failed", retryable: false } }, + payload: { + error: { code: "failed", message: "failed", retryable: false }, + recoverable: false, + }, }, ], }), @@ -817,17 +966,23 @@ describe("DriverInstanceSocket lifecycle", () => { await expect( socket.pushEvents({ events: [{ ...completed, delivery: "best_effort" }] }), ).rejects.toThrow("must use lossless delivery"); - expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toBeNull(); + expect(barrierCalls).toBe(1); - RpcWebSocket.acceptedEventCounts = [1, 0]; - await expect(socket.pushEvents({ events: [delta, completed] })).rejects.toThrow("no progress"); - expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); - expect(socket.selectedRunEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + await expect(socket.pushEvents({ events: [delta, completed] })).rejects.toThrow( + "must be the only event", + ); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toBeNull(); + expect(barrierCalls).toBe(1); await expect(socket.pushEvents({ events: [completed] })).resolves.toMatchObject({ accepted: [{ type: "run.completed" }], }); - expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + expect(barrierCalls).toBe(2); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toMatchObject({ + phase: "acked", + value: { status: "completed" }, + }); await expect(socket.pushEvents({ events: [delta] })).rejects.toThrow( "cannot target a terminated run", ); @@ -847,19 +1002,22 @@ describe("DriverInstanceSocket lifecycle", () => { protocolVersion: driverBootPayload.protocolVersion, startedAt: new Date(0).toISOString(), }); - socket.beginRun(DRIVER_TEST_IDS.runId); + const ticket = socket.beginRun(DRIVER_TEST_IDS.runId); await socket.pushEvents({ events: [{ kind: "run.completed", payload: { stopReason: "end_turn" } }], }); - expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow("another run is active"); + expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow("already active"); expect(socket.currentRunId()).toBe(DRIVER_TEST_IDS.runId); - expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toMatchObject({ + phase: "acked", + value: { status: "completed" }, + }); - socket.endRun(DRIVER_TEST_IDS.runId); + socket.releaseRun(ticket, "command_acked"); socket.beginRun(DRIVER_TEST_IDS.secondRunId); expect(socket.currentRunId()).toBe(DRIVER_TEST_IDS.secondRunId); - expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)).toBeNull(); }); test("reserves a terminal before its RPC and serializes the matching control terminal", async () => { @@ -878,7 +1036,10 @@ describe("DriverInstanceSocket lifecycle", () => { }; const failed: DriverEventInput = { kind: "run.failed", - payload: { error: { code: "failed", message: "failed", retryable: false } }, + payload: { + error: { code: "failed", message: "failed", retryable: false }, + recoverable: false, + }, }; const failure = { code: "failed", @@ -889,10 +1050,12 @@ describe("DriverInstanceSocket lifecycle", () => { const eventTerminal = socket.pushEvents({ events: [completed] }); await RpcWebSocket.stalled.promise; - expect(socket.selectedRunEventTerminal(DRIVER_TEST_IDS.runId)).toBe("completed"); - expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toMatchObject({ + phase: "selected", + value: { status: "completed" }, + }); await expect(socket.pushEvents({ events: [failed] })).rejects.toThrow( - "cannot target a terminated run", + "conflicts with the selected terminal", ); const matchingControl = socket.completeRun(); const eventTerminalOutcome = eventTerminal.then( @@ -903,7 +1066,7 @@ describe("DriverInstanceSocket lifecycle", () => { () => null, (error: unknown) => error, ); - await expect(socket.failRun(failure)).resolves.toBeUndefined(); + await expect(socket.failRun(failure)).rejects.toThrow("conflicts"); await Bun.sleep(0); const firstWire = PendingWebSocket.instances[0] as RpcWebSocket; @@ -919,7 +1082,7 @@ describe("DriverInstanceSocket lifecycle", () => { "message", expect.stringContaining("connection changed"), ); - expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal?.phase).toBe("selected"); await socket.connect(); await socket.hello({ @@ -929,7 +1092,7 @@ describe("DriverInstanceSocket lifecycle", () => { startedAt: new Date(0).toISOString(), }); await expect(socket.pushEvents({ events: [failed] })).rejects.toThrow( - "cannot target a terminated run", + "conflicts with the selected terminal", ); expect((PendingWebSocket.instances[1] as RpcWebSocket).paths).toEqual(["/driver/hello"]); }); @@ -939,7 +1102,7 @@ describe("DriverInstanceSocket lifecycle", () => { ["failRun", "/driver/failRun"], ] as const)("does not begin another run after an in-flight %s", async (selected, path) => { const socket = await connectRpcSocket(); - socket.beginRun(DRIVER_TEST_IDS.runId); + const ticket = socket.beginRun(DRIVER_TEST_IDS.runId); const failure = { code: "test.failure", details: {}, @@ -950,23 +1113,19 @@ describe("DriverInstanceSocket lifecycle", () => { const terminal = selected === "completeRun" ? socket.completeRun() : socket.failRun(failure); await RpcWebSocket.stalled.promise; - socket.endRun(DRIVER_TEST_IDS.runId); - expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow( - "control terminal has been selected", - ); + socket.releaseRun(ticket, "driver_failing"); + expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow("instance terminal"); expect(socket.currentRunId()).toBeNull(); expect(selected === "completeRun" ? socket.completeRun() : socket.failRun(failure)).toBe( terminal, ); await expect( selected === "completeRun" ? socket.failRun(failure) : socket.completeRun(), - ).resolves.toBeUndefined(); + ).rejects.toThrow("conflicts"); (PendingWebSocket.instances[0] as RpcWebSocket).close(1006, "terminal response lost"); await expect(terminal).rejects.toThrow("terminal response lost"); - expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow( - "control terminal has been selected", - ); + expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow("instance terminal"); }); test("does not redirect a queued run event after the active run ends", async () => { @@ -977,7 +1136,7 @@ describe("DriverInstanceSocket lifecycle", () => { protocolVersion: driverBootPayload.protocolVersion, startedAt: new Date(0).toISOString(), }); - socket.beginRun(DRIVER_TEST_IDS.runId); + const ticket = socket.beginRun(DRIVER_TEST_IDS.runId); RpcWebSocket.stalledPath = "/driver/pushEvents"; const controller = new AbortController(); const blocker = socket.pushEvents({ @@ -999,7 +1158,7 @@ describe("DriverInstanceSocket lifecycle", () => { (error: unknown) => error, ); - socket.endRun(DRIVER_TEST_IDS.runId); + socket.releaseRun(ticket, "driver_failing"); socket.beginRun(DRIVER_TEST_IDS.secondRunId); controller.abort(new Error("release old run")); await expect(blocker).rejects.toThrow(); @@ -1009,7 +1168,7 @@ describe("DriverInstanceSocket lifecycle", () => { "message", expect.stringContaining("active run changed"), ); - expect(socket.runEventTerminal(DRIVER_TEST_IDS.runId)).toBeNull(); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)).toBeNull(); expect( (PendingWebSocket.instances[0] as RpcWebSocket).paths.filter( (path) => path === "/driver/pushEvents", @@ -1029,7 +1188,7 @@ describe("DriverInstanceSocket lifecycle", () => { ).resolves.toMatchObject({ accepted: [{ type: "message.delta" }] }); }); - test("does not send a queued control terminal after its run ends", async () => { + test("keeps a queued instance terminal owned after its run ends", async () => { const socket = await connectRpcSocket(); await socket.hello({ capabilities: [], @@ -1037,7 +1196,7 @@ describe("DriverInstanceSocket lifecycle", () => { protocolVersion: driverBootPayload.protocolVersion, startedAt: new Date(0).toISOString(), }); - socket.beginRun(DRIVER_TEST_IDS.runId); + const ticket = socket.beginRun(DRIVER_TEST_IDS.runId); RpcWebSocket.stalledPath = "/driver/pushEvents"; const controller = new AbortController(); const blocker = socket.pushEvents({ @@ -1053,17 +1212,15 @@ describe("DriverInstanceSocket lifecycle", () => { await RpcWebSocket.stalled.promise; const oldTerminal = socket.completeRun(); - socket.endRun(DRIVER_TEST_IDS.runId); - expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow( - "control terminal has been selected", - ); + socket.releaseRun(ticket, "driver_failing"); + expect(() => socket.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow("instance terminal"); controller.abort(new Error("release old run")); await expect(blocker).rejects.toThrow(); - await expect(oldTerminal).rejects.toThrow("active run changed"); + await expect(oldTerminal).resolves.toBeUndefined(); const paths = (PendingWebSocket.instances[0] as RpcWebSocket).paths; expect(paths.filter((path) => path === "/driver/pushEvents")).toHaveLength(1); - expect(paths).not.toContain("/driver/completeRun"); + expect(paths).toContain("/driver/completeRun"); expect(socket.currentRunId()).toBeNull(); }); diff --git a/tests/driver-permission-broker.test.ts b/tests/driver-permission-broker.test.ts index e1e323c..09f4009 100644 --- a/tests/driver-permission-broker.test.ts +++ b/tests/driver-permission-broker.test.ts @@ -23,6 +23,7 @@ interface RecordingSocket extends DriverRuntimeEventPort { function acceptEvents(events: readonly DriverEventInput[]): DriverEventBatchOutput { return { accepted: events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), @@ -222,7 +223,7 @@ describe("DriverPermissionBroker", () => { pushedEvents.push(...events); return { accepted: events.map((event) => ({ - eventId: event.sourceEventId, + eventId: event.sourceEventId!, seq: (sequence += 1), type: event.kind, })), @@ -279,11 +280,11 @@ describe("DriverPermissionBroker", () => { expect(() => new DriverPermissionBroker(() => null, { requestTimeoutMs: 0 })).not.toThrow(); }); - test.each(["resolve", "abort", "rejectAll", "timeout", "publish failure"] as const)( + test.each(["resolve", "abort", "rejectAll", "timeout", "publish retry"] as const)( "returns pending capacity after %s", async (mode) => { const controller = new AbortController(); - let failPublish = mode === "publish failure"; + let failPublish = mode === "publish retry"; const socket: DriverRuntimeEventPort = { currentRunId: () => runId, pushEvents: async ({ events }) => { @@ -308,8 +309,9 @@ describe("DriverPermissionBroker", () => { controller.abort(); await expect(first).resolves.toBe("reject_once"); break; - case "publish failure": - await expect(first).rejects.toBeInstanceOf(PermissionEventDeliveryError); + case "publish retry": + expect(broker.resolve(permissionInput.requestId, "allow_once")).toBe(true); + await expect(first).resolves.toBe("allow_once"); break; case "rejectAll": broker.rejectAll(); @@ -509,7 +511,7 @@ describe("DriverPermissionBroker", () => { phase: "resolved", requestId: permissionInput.requestId, }); - expect((outcome.error as Error).cause).toBe(deliveryFailure); + expect(((outcome.error as Error).cause as Error).cause).toBe(deliveryFailure); } failResolution = false; @@ -687,40 +689,53 @@ describe("DriverPermissionBroker", () => { expect(broker.hasPending()).toBe(false); }); - test("wraps a rejected request delivery and releases its identity", async () => { - const deliveryFailure = new Error("event sink unavailable"); - let fail = true; + test("replays one stable lifecycle when persisted permission ACKs are lost", async () => { + const attempts: DriverEventInput[][] = []; + const persisted = new Map(); + const phaseAttempts = new Map(); const broker = new DriverPermissionBroker(() => null); const socket: DriverRuntimeEventPort = { currentRunId: () => runId, pushEvents: async ({ events }) => { - if (fail) { - fail = false; - throw deliveryFailure; + const owned = structuredClone(events); + attempts.push(owned); + for (const event of owned) { + const sourceEventId = event.sourceEventId!; + const content = JSON.stringify(event); + const previous = persisted.get(sourceEventId); + expect(previous === undefined || previous === content).toBe(true); + persisted.set(sourceEventId, content); } - return acceptEvents(events); + const phase = owned[0]!.kind; + const attempt = (phaseAttempts.get(phase) ?? 0) + 1; + phaseAttempts.set(phase, attempt); + if (attempt === 1) { + throw new Error(`${phase} ACK lost after persistence`); + } + return acceptEvents(owned); }, }; - const outcome = await settlePromiseWithTimeout(broker.request(socket, permissionInput), { - label: "rejected permission request delivery", - timeoutMs: 100, - }); - expect(outcome.status).toBe("failed"); - if (outcome.status === "failed") { - expect(outcome.error).toBeInstanceOf(PermissionEventDeliveryError); - expect(outcome.error).toMatchObject({ - phase: "requested", - requestId: permissionInput.requestId, - }); - expect((outcome.error as Error).cause).toBe(deliveryFailure); + for (let replay = 0; replay < 2; replay += 1) { + const request = broker.request(socket, permissionInput); + await Promise.resolve(); + broker.rejectAll(); + await expect(request).resolves.toBe("reject_once"); } - const retry = broker.request(socket, permissionInput); - await Promise.resolve(); - expect(broker.resolve(permissionInput.requestId, "reject_once")).toBe(true); - await expect(retry).resolves.toBe("reject_once"); + expect(attempts.map((events) => events.map(({ kind }) => kind))).toEqual([ + ["permission.requested"], + ["permission.requested"], + ["permission.resolved", "diagnostic.reported"], + ["permission.resolved", "diagnostic.reported"], + ["permission.requested"], + ["permission.resolved", "diagnostic.reported"], + ]); + expect(persisted.size).toBe(3); + expect( + [...persisted.keys()].every((sourceEventId) => sourceEventId.startsWith("permission:")), + ).toBe(true); }); test("rejects unsupported interactive permission requests instead of allowing them", async () => { diff --git a/tests/driver-permission-policy.test.ts b/tests/driver-permission-policy.test.ts index 8c65ba0..0dadf18 100644 --- a/tests/driver-permission-policy.test.ts +++ b/tests/driver-permission-policy.test.ts @@ -54,7 +54,7 @@ describe("driver permission policy", () => { const legacyPayload: Record = structuredClone(driverBootPayload); legacyPayload["protocolVersion"] = 1; - expect(() => parseDriverBootPayload(legacyPayload)).toThrow(/protocolVersion must be 2/); + expect(() => parseDriverBootPayload(legacyPayload)).toThrow(/protocolVersion must be 3/); }); test("isDriverFullAccess reflects the payload", () => { diff --git a/tests/driver-rpc-wire-v3.test.ts b/tests/driver-rpc-wire-v3.test.ts new file mode 100644 index 0000000..f85a59a --- /dev/null +++ b/tests/driver-rpc-wire-v3.test.ts @@ -0,0 +1,629 @@ +import { describe, expect, test } from "bun:test"; + +import { DRIVER_PROTOCOL_VERSION } from "../src/protocol/boot"; +import { driverRuntimeRpcSchemas } from "../src/protocol/orpc"; +import { + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + measureRuntimeCommandJson, +} from "../src/runtime-command"; +import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; + +const invalidPositiveSafeIntegers = [ + 0, + -1, + 0.5, + Number.NaN, + Number.POSITIVE_INFINITY, + Number.NEGATIVE_INFINITY, + Number.MAX_SAFE_INTEGER + 1, +] as const; +const invalidNonNegativeSafeIntegers = invalidPositiveSafeIntegers.slice(1); + +function helloInput(overrides: Record = {}) { + return { + capabilities: [], + driverVersion: "test", + pid: 1, + protocolVersion: DRIVER_PROTOCOL_VERSION, + runtime: "acp-fallback", + startedAt: "2026-08-29T00:00:00.000Z", + ...overrides, + }; +} + +function helloOutput(runConfig: Record = {}, runId: string | null = null) { + return { + acceptedCapabilities: [], + connectionId: "connection-1", + driverInstanceId: "driver-1", + heartbeatIntervalMs: 250, + runConfig: { + commandLeaseMs: 0, + envPolicy: "strict", + eventBatchMaxSize: 1, + organizationPath: "/workspace", + ...runConfig, + }, + runId, + }; +} + +function heartbeatInput(pid: number, at = "2026-08-29T00:00:00.000Z") { + return { at, pid, reason: "interval" }; +} + +function readyInput(pid: number) { + return { + at: "2026-08-29T00:00:00.000Z", + driverInstanceId: "driver-1", + pid, + }; +} + +function logBatch(seq: number) { + return { + driverInstanceId: "driver-1", + logs: [{ level: "info", message: "message", seq, timestamp: "now" }], + }; +} + +function eventBatchOutput(seq: number, type = "message.delta") { + return { accepted: [{ eventId: "source-1", seq, type }] }; +} + +function diagnosticEvent(payload: Record = { message: "ok" }) { + return { + actor: "driver", + delivery: "lossless", + driverInstanceId: DRIVER_TEST_IDS.driverInstanceId, + id: "01J0000000000000000000000G", + kind: "diagnostic.reported", + occurredAt: "2026-08-29T00:00:00.000Z", + origin: "driver", + payload, + runId: DRIVER_TEST_IDS.runId, + schemaVersion: "2026-08-29", + sessionId: DRIVER_TEST_IDS.sessionId, + visibility: "owner_debug", + }; +} + +function textFieldAtJsonSize(targetBytes: number, create: (text: string) => Value): Value { + const remaining = targetBytes - measureRuntimeCommandJson(create("")); + const value = create("x".repeat(remaining)); + + expect(measureRuntimeCommandJson(value)).toBe(targetBytes); + return value; +} + +describe("Driver RPC wire v3", () => { + test.each([2, 4] as const)("rejects protocol version %d", (protocolVersion) => { + expect( + driverRuntimeRpcSchemas.driver.hello.input.safeParse(helloInput({ protocolVersion })).success, + ).toBeFalse(); + }); + + test.each(invalidPositiveSafeIntegers)( + "rejects %p at every positive safe-integer field", + (value) => { + expect( + driverRuntimeRpcSchemas.driver.hello.input.safeParse(helloInput({ pid: value })).success, + ).toBeFalse(); + expect( + driverRuntimeRpcSchemas.driver.heartbeat.input.safeParse(heartbeatInput(value)).success, + ).toBeFalse(); + expect(driverRuntimeRpcSchemas.driver.ready.input.safeParse(readyInput(value)).success).toBe( + false, + ); + expect( + driverRuntimeRpcSchemas.driver.hello.output.safeParse( + helloOutput({ eventBatchMaxSize: value }), + ).success, + ).toBeFalse(); + expect( + driverRuntimeRpcSchemas.driver.claimExternalToolEffect.output.safeParse({ + attempt: value, + effectId: "effect-1", + idempotencyKey: "idempotency-1", + kind: "claimed", + }).success, + ).toBeFalse(); + }, + ); + + test.each([ + [64, true], + [65, false], + ] as const)("validates wire-safe event batch limit %d", (eventBatchMaxSize, accepted) => { + expect( + driverRuntimeRpcSchemas.driver.hello.output.safeParse(helloOutput({ eventBatchMaxSize })) + .success, + ).toBe(accepted); + }); + + test.each([ + 0, + 249, + 250.5, + Number.NaN, + Number.POSITIVE_INFINITY, + Number.NEGATIVE_INFINITY, + Number.MAX_SAFE_INTEGER + 1, + ] as const)("rejects invalid heartbeat interval %p", (heartbeatIntervalMs) => { + expect( + driverRuntimeRpcSchemas.driver.hello.output.safeParse({ + ...helloOutput(), + heartbeatIntervalMs, + }).success, + ).toBeFalse(); + }); + + test.each(invalidNonNegativeSafeIntegers)( + "rejects %p at every non-negative safe-integer field", + (value) => { + expect( + driverRuntimeRpcSchemas.driver.hello.output.safeParse( + helloOutput({ commandLeaseMs: value }), + ).success, + ).toBeFalse(); + expect( + driverRuntimeRpcSchemas.driver.heartbeat.output.safeParse({ + heartbeatCount: value, + ok: true, + }).success, + ).toBeFalse(); + expect(driverRuntimeRpcSchemas.driver.pushLogs.input.safeParse(logBatch(value)).success).toBe( + false, + ); + expect( + driverRuntimeRpcSchemas.driver.pushEvents.output.safeParse(eventBatchOutput(value)).success, + ).toBeFalse(); + }, + ); + + test("accepts zero at every non-negative safe-integer field", () => { + expect(driverRuntimeRpcSchemas.driver.hello.output.safeParse(helloOutput()).success).toBeTrue(); + expect( + driverRuntimeRpcSchemas.driver.heartbeat.output.safeParse({ heartbeatCount: 0, ok: true }) + .success, + ).toBeTrue(); + expect(driverRuntimeRpcSchemas.driver.pushLogs.input.safeParse(logBatch(0)).success).toBeTrue(); + expect( + driverRuntimeRpcSchemas.driver.pushEvents.output.safeParse(eventBatchOutput(0)).success, + ).toBeTrue(); + }); + + test.each([ + ["event", "events", { event: diagnosticEvent(), eventId: "source-1" }], + ["log", "logs", logBatch(0).logs[0]], + ] as const)("bounds %s batches at 64 entries", (_label, field, entry) => { + const schema = + field === "events" + ? driverRuntimeRpcSchemas.driver.pushEvents.input + : driverRuntimeRpcSchemas.driver.pushLogs.input; + const input = (length: number) => ({ + driverInstanceId: "driver-1", + [field]: Array.from({ length }, () => entry), + }); + + expect(schema.safeParse(input(64)).success).toBeTrue(); + expect(schema.safeParse(input(65)).success).toBeFalse(); + }); + + test.each([ + [ + "failure message", + driverRuntimeRpcSchemas.driver.failRun.input, + { + driverInstanceId: "driver-1", + error: { code: "failed", details: {}, message: "", retryable: false }, + runId: "run-1", + }, + ], + ["receipt type", driverRuntimeRpcSchemas.driver.pushEvents.output, eventBatchOutput(0, "")], + ] as const)("rejects an empty %s", (_label, schema, input) => { + expect(schema.safeParse(input).success).toBeFalse(); + }); + + test("rejects an unknown receipt event type", () => { + expect( + driverRuntimeRpcSchemas.driver.pushEvents.output.safeParse( + eventBatchOutput(0, "future.event"), + ).success, + ).toBeFalse(); + }); + + test.each([ + [driverRuntimeRpcSchemas.driver.completeRun.input, { driverInstanceId: "driver-1" }], + [driverRuntimeRpcSchemas.driver.completeRun.input, { driverInstanceId: "driver-1", runId: "" }], + [ + driverRuntimeRpcSchemas.driver.failRun.input, + { + driverInstanceId: "driver-1", + error: { code: "failed", details: {}, message: "failed", retryable: false }, + }, + ], + [ + driverRuntimeRpcSchemas.driver.failRun.input, + { + driverInstanceId: "driver-1", + error: { code: "failed", details: {}, message: "failed", retryable: false }, + runId: "", + }, + ], + ] as const)("requires an exact run id for every control terminal", (schema, input) => { + expect(schema.safeParse(input).success).toBeFalse(); + }); + + test.each([ + ["missing", { accepted: [{ seq: 0, type: "message.delta" }] }], + ["empty", { accepted: [{ eventId: "", seq: 0, type: "message.delta" }] }], + ["non-string", { accepted: [{ eventId: 1, seq: 0, type: "message.delta" }] }], + ] as const)("rejects %s receipt eventId", (_label, input) => { + expect(driverRuntimeRpcSchemas.driver.pushEvents.output.safeParse(input).success).toBeFalse(); + }); + + test("rejects empty handshake identity fields", () => { + expect( + driverRuntimeRpcSchemas.driver.hello.input.safeParse(helloInput({ startedAt: "" })).success, + ).toBeFalse(); + expect( + driverRuntimeRpcSchemas.driver.heartbeat.input.safeParse(heartbeatInput(1, "")).success, + ).toBeFalse(); + expect(driverRuntimeRpcSchemas.driver.hello.output.safeParse(helloOutput({}, "")).success).toBe( + false, + ); + }); + + test("bounds run and mutually exclusive command terminal payloads", () => { + const error = textFieldAtJsonSize( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + (message) => ({ code: "failed", details: {}, message, retryable: false }), + ); + const result = textFieldAtJsonSize( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + (outputText) => ({ + outputText, + requestId: "request-1", + serverId: "server-1", + toolName: "tool-1", + }), + ); + const commandUpdate = driverRuntimeRpcSchemas.driver.commandUpdate.input; + const failRun = driverRuntimeRpcSchemas.driver.failRun.input; + + expect(failRun.safeParse({ driverInstanceId: "driver-1", error, runId: "run-1" }).success).toBe( + true, + ); + expect( + failRun.safeParse({ + driverInstanceId: "driver-1", + error: { ...error, message: `${error.message}x` }, + runId: "run-1", + }).success, + ).toBeFalse(); + + expect( + commandUpdate.safeParse({ + commandId: "command-1", + driverInstanceId: "driver-1", + error, + status: "failed", + }).success, + ).toBeTrue(); + expect( + commandUpdate.safeParse({ + commandId: "command-1", + driverInstanceId: "driver-1", + error: { ...error, message: `${error.message}x` }, + status: "failed", + }).success, + ).toBeFalse(); + expect( + commandUpdate.safeParse({ + commandId: "command-1", + driverInstanceId: "driver-1", + result, + status: "completed", + }).success, + ).toBeTrue(); + expect( + commandUpdate.safeParse({ + commandId: "command-1", + driverInstanceId: "driver-1", + result: { ...result, outputText: `${result.outputText}x` }, + status: "completed", + }).success, + ).toBeFalse(); + expect( + commandUpdate.safeParse({ + commandId: "command-1", + driverInstanceId: "driver-1", + error, + result: null, + status: "failed", + }).success, + ).toBeFalse(); + }); + + test.each(["queued", "delivered", "expired"] as const)( + "rejects database-only command status %s on Driver updates", + (status) => { + expect( + driverRuntimeRpcSchemas.driver.commandUpdate.input.safeParse({ + commandId: "command-1", + driverInstanceId: "driver-1", + status, + }).success, + ).toBeFalse(); + }, + ); + + test("enforces command update payloads by terminal status", () => { + const schema = driverRuntimeRpcSchemas.driver.commandUpdate.input; + const identity = { commandId: "command-1", driverInstanceId: "driver-1" }; + const error = { code: "failed", details: {}, message: "failed", retryable: false }; + const result = { requestId: "request-1" }; + + for (const valid of [ + { ...identity, status: "accepted" }, + { ...identity, status: "cancelled" }, + { ...identity, status: "completed" }, + { ...identity, result, status: "completed" }, + { ...identity, error, status: "failed" }, + ]) { + expect(schema.safeParse(valid).success).toBeTrue(); + } + + for (const invalid of [ + { ...identity, error, status: "accepted" }, + { ...identity, result, status: "accepted" }, + { ...identity, error, status: "cancelled" }, + { ...identity, result, status: "cancelled" }, + { ...identity, error, status: "completed" }, + { ...identity, result: null, status: "completed" }, + { ...identity, status: "failed" }, + { ...identity, error, result, status: "failed" }, + ]) { + expect(schema.safeParse(invalid).success).toBeFalse(); + } + }); + + test("rejects duplicate capabilities on both sides of the handshake", () => { + const capability = { id: "text_stream", status: "supported", version: 1 }; + + expect( + driverRuntimeRpcSchemas.driver.hello.input.safeParse( + helloInput({ capabilities: [capability, capability] }), + ).success, + ).toBeFalse(); + expect( + driverRuntimeRpcSchemas.driver.hello.output.safeParse({ + ...helloOutput(), + acceptedCapabilities: [capability, capability], + }).success, + ).toBeFalse(); + }); + + test("rejects unknown keys on every fixed RPC input and output object", () => { + const failure = { code: "failed", details: {}, message: "failed", retryable: false }; + const claim = { + attempt: 1, + effectId: "effect-1", + idempotencyKey: "idempotency-1", + kind: "claimed", + }; + const cases = [ + [ + driverRuntimeRpcSchemas.driver.observeExternalToolEffect.input, + { commandId: "command-1", driverInstanceId: "driver-1" }, + ], + [ + driverRuntimeRpcSchemas.driver.observeExternalToolEffect.output, + { effectId: "effect-1", kind: "intent" }, + ], + [ + driverRuntimeRpcSchemas.driver.claimExternalToolEffect.input, + { + claimToken: "00000000-0000-4000-8000-000000000001", + commandId: "command-1", + driverInstanceId: "driver-1", + }, + ], + [driverRuntimeRpcSchemas.driver.claimExternalToolEffect.output, claim], + [ + driverRuntimeRpcSchemas.driver.commandUpdate.input, + { commandId: "command-1", driverInstanceId: "driver-1", status: "accepted" }, + ], + [driverRuntimeRpcSchemas.driver.commandUpdate.output, { ok: true }], + [ + driverRuntimeRpcSchemas.driver.completeRun.input, + { driverInstanceId: "driver-1", runId: "run-1" }, + ], + [driverRuntimeRpcSchemas.driver.completeRun.output, { ok: true }], + [ + driverRuntimeRpcSchemas.driver.failRun.input, + { driverInstanceId: "driver-1", error: failure, runId: "run-1" }, + ], + [driverRuntimeRpcSchemas.driver.failRun.output, { ok: true }], + [driverRuntimeRpcSchemas.driver.heartbeat.input, heartbeatInput(1)], + [driverRuntimeRpcSchemas.driver.heartbeat.output, { heartbeatCount: 0, ok: true }], + [driverRuntimeRpcSchemas.driver.hello.input, helloInput()], + [driverRuntimeRpcSchemas.driver.hello.output, helloOutput()], + [ + driverRuntimeRpcSchemas.driver.settleExternalToolEffect.input, + { + claimToken: "00000000-0000-4000-8000-000000000001", + commandId: "command-1", + driverInstanceId: "driver-1", + effectId: "effect-1", + settlement: { kind: "unknown" }, + }, + ], + [ + driverRuntimeRpcSchemas.driver.settleExternalToolEffect.output, + { effectId: "effect-1", kind: "unknown" }, + ], + [ + driverRuntimeRpcSchemas.driver.pushEvents.input, + { + driverInstanceId: "driver-1", + events: [{ event: diagnosticEvent(), eventId: "source-1" }], + }, + ], + [driverRuntimeRpcSchemas.driver.pushEvents.output, eventBatchOutput(0)], + [driverRuntimeRpcSchemas.driver.pushLogs.input, logBatch(0)], + [driverRuntimeRpcSchemas.driver.pushLogs.output, { ok: true }], + [driverRuntimeRpcSchemas.driver.ready.input, readyInput(1)], + [driverRuntimeRpcSchemas.driver.ready.output, { ok: true }], + [driverRuntimeRpcSchemas.driverInstance.nextCommand.input, { driverInstanceId: "driver-1" }], + [driverRuntimeRpcSchemas.driverInstance.nextCommand.output, { command: null }], + ] as const; + + for (const [schema, value] of cases) { + expect(schema.safeParse({ ...value, future: true }).success).toBeFalse(); + } + }); + + test("rejects unknown keys on nested fixed RPC objects but preserves event payload extensions", () => { + const event = diagnosticEvent({ future: { nested: true }, message: "ok" }); + const nestedCases = [ + [ + driverRuntimeRpcSchemas.driver.hello.input, + helloInput({ + capabilities: [{ future: true, id: "text_stream", status: "supported", version: 1 }], + }), + ], + [driverRuntimeRpcSchemas.driver.hello.output, helloOutput({ future: true })], + [ + driverRuntimeRpcSchemas.driver.failRun.input, + { + driverInstanceId: "driver-1", + error: { code: "failed", details: {}, future: true, message: "failed", retryable: false }, + runId: "run-1", + }, + ], + [ + driverRuntimeRpcSchemas.driver.commandUpdate.input, + { + commandId: "command-1", + driverInstanceId: "driver-1", + result: { future: true, requestId: "request-1" }, + status: "completed", + }, + ], + [ + driverRuntimeRpcSchemas.driver.settleExternalToolEffect.input, + { + claimToken: "00000000-0000-4000-8000-000000000001", + commandId: "command-1", + driverInstanceId: "driver-1", + effectId: "effect-1", + settlement: { future: true, kind: "unknown" }, + }, + ], + [ + driverRuntimeRpcSchemas.driver.pushLogs.input, + { driverInstanceId: "driver-1", logs: [{ ...logBatch(0).logs[0], future: true }] }, + ], + [ + driverRuntimeRpcSchemas.driver.pushLogs.input, + { + driverInstanceId: "driver-1", + logs: [{ ...logBatch(0).logs[0], context: { future: true } }], + }, + ], + [ + driverRuntimeRpcSchemas.driver.pushLogs.input, + { + driverInstanceId: "driver-1", + logs: [ + { ...logBatch(0).logs[0], error: { future: true, message: "failed", name: "Error" } }, + ], + }, + ], + [ + driverRuntimeRpcSchemas.driver.pushEvents.input, + { driverInstanceId: "driver-1", events: [{ event, eventId: "source-1", future: true }] }, + ], + [ + driverRuntimeRpcSchemas.driver.pushEvents.input, + { + driverInstanceId: "driver-1", + events: [{ event: { ...event, future: true }, eventId: "source-1" }], + }, + ], + [ + driverRuntimeRpcSchemas.driver.pushEvents.input, + { + driverInstanceId: "driver-1", + events: [ + { + event: { ...event, native: { future: true, provider: "openai" } }, + eventId: "source-1", + }, + ], + }, + ], + [ + driverRuntimeRpcSchemas.driver.pushEvents.output, + { accepted: [{ ...eventBatchOutput(0).accepted[0], future: true }] }, + ], + ] as const; + + for (const [schema, value] of nestedCases) { + expect(schema.safeParse(value).success).toBeFalse(); + } + + expect( + driverRuntimeRpcSchemas.driver.pushEvents.input.safeParse({ + driverInstanceId: "driver-1", + events: [{ event, eventId: "source-1" }], + }).success, + ).toBeTrue(); + }); + + test("preserves empty log and tracing strings", () => { + expect( + driverRuntimeRpcSchemas.driver.pushLogs.input.safeParse({ + driverInstanceId: "driver-1", + logs: [ + { + context: { spanId: "", traceId: "" }, + error: { message: "", name: "" }, + level: "error", + message: "", + seq: 0, + timestamp: "now", + }, + ], + }).success, + ).toBeTrue(); + }); + + test("rejects the previous runtime event schema", () => { + expect( + driverRuntimeRpcSchemas.driver.pushEvents.input.safeParse({ + driverInstanceId: DRIVER_TEST_IDS.driverInstanceId, + events: [ + { + event: { + actor: "driver", + delivery: "lossless", + driverInstanceId: DRIVER_TEST_IDS.driverInstanceId, + id: "01J0000000000000000000000G", + kind: "diagnostic.reported", + occurredAt: "2026-08-29T00:00:00.000Z", + origin: "driver", + payload: { message: "ok" }, + schemaVersion: "2026-05-26", + sessionId: DRIVER_TEST_IDS.sessionId, + visibility: "owner_debug", + }, + eventId: "source-1", + }, + ], + }).success, + ).toBeFalse(); + }); +}); diff --git a/tests/driver-runtime-boundary-dispatcher.test.ts b/tests/driver-runtime-boundary-dispatcher.test.ts index bf05428..7925cb6 100644 --- a/tests/driver-runtime-boundary-dispatcher.test.ts +++ b/tests/driver-runtime-boundary-dispatcher.test.ts @@ -7,6 +7,7 @@ import { import { DriverRuntimeStateMachine, DriverTurnCancellationCleanupError, + DriverTurnCancelledError, } from "../src/core/driver-runtime-state"; import type { RuntimeCommand } from "../src/runtime-command"; import { settlePromiseWithTimeout } from "../src/utils/async"; @@ -15,9 +16,81 @@ import { FakeDriverRuntimeIo, createBackend, createDispatcher, + waitForUpdate, } from "./driver-runtime-boundary-fixtures"; describe("driver runtime boundary", () => { + test("parses a custom command source before acknowledgement or business side effects", async () => { + const backend = createBackend(); + const invalid = { + argumentsJson: "{}", + commandId: "invalid-custom-source", + extra: true, + kind: "mcp.execute", + requestId: "invalid-custom-source-request", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: "invalid-custom-source-tool", + toolName: "createIssue", + } as unknown as RuntimeCommand; + const socket = new FakeDriverRuntimeIo([invalid], DRIVER_TEST_IDS.runId); + let preparations = 0; + const { dispatcher, logger } = createDispatcher({ + backend, + mcpPrepare: async () => { + preparations += 1; + throw new Error("invalid command reached MCP preparation"); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await expect(dispatcher.run(socket, logger)).rejects.toThrow( + "runtime command.extra is not allowed", + ); + + expect(socket.updates).toEqual([]); + expect(preparations).toBe(0); + }); + + test("projects durable attachment provenance to provider-neutral text", async () => { + const backend = createBackend(); + const providerInputs: unknown[] = []; + backend.handleInput = async (context, input, runId) => { + providerInputs.push(structuredClone(input)); + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `attachment-projection.completed:${runId}`, + }, + ], + }); + }; + const command = { + commandId: "attachment-projection", + input: { attachmentIds: ["file-1"], text: "materialized attachment" }, + kind: "input.start", + requestId: "attachment-projection-request", + runId: DRIVER_TEST_IDS.runId, + } as unknown as RuntimeCommand; + const socket = new FakeDriverRuntimeIo([command]); + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => socket.isDrained(), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await dispatcher.run(socket, logger); + await waitForUpdate( + socket, + (update) => update.commandId === "attachment-projection" && update.status === "completed", + ); + + expect(providerInputs).toEqual([{ text: "materialized attachment" }]); + }); + test("delivers a session.stop terminal only after the shutdown barrier", async () => { const cleanupEntered = Promise.withResolvers(); const releaseCleanup = Promise.withResolvers(); @@ -126,7 +199,7 @@ describe("driver runtime boundary", () => { return result; }; let permission: Promise<"allow_once" | "reject_once"> | null = null; - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId) => { permission = permissions.request(socket, { rawInput: null, requestId: "stop-permission", @@ -135,6 +208,17 @@ describe("driver runtime boundary", () => { toolKind: "test", }); await permission; + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.cancelled", + payload: { reason: "test.stop", status: "cancelled" }, + runId, + sourceEventId: `stop-permission.cancelled:${runId}`, + }, + ], + }); + throw new DriverTurnCancelledError("test.stop"); }; const { dispatcher, logger } = createDispatcher({ backend, @@ -152,9 +236,11 @@ describe("driver runtime boundary", () => { releaseRequested.resolve(); await expect(Promise.all([permission!, run])).resolves.toEqual(["reject_once", undefined]); expect( - order.filter((kind) => - ["permission.resolved", "run.cancelled", "cleanup", "control.completeRun"].includes(kind), - ), + order + .filter((kind) => + ["permission.resolved", "run.cancelled", "cleanup", "control.completeRun"].includes(kind), + ) + .filter((kind, index, values) => index === 0 || kind !== values[index - 1]), ).toEqual(["permission.resolved", "run.cancelled", "cleanup", "control.completeRun"]); expect(socket.updates.at(-1)).toMatchObject({ commandId: "stop-with-permission", @@ -193,6 +279,7 @@ describe("driver runtime boundary", () => { commandId: "cancel-with-permission-failure", kind: "turn.cancel", reason: "test cancellation", + runId: DRIVER_TEST_IDS.runId, }, ]); const { dispatcher, logger } = createDispatcher({ @@ -260,6 +347,7 @@ describe("driver runtime boundary", () => { commandId: "cancel-with-cleanup-failure", kind: "turn.cancel", reason: "test cancellation", + runId: DRIVER_TEST_IDS.runId, }, ]); const { dispatcher, logger } = createDispatcher({ @@ -291,10 +379,22 @@ describe("driver runtime boundary", () => { let cancellationRequested = false; let sideEffects = 0; const backend = createBackend(); - backend.handleInput = async (_context, _input, _runId, signal) => { + backend.handleInput = async (context, _input, runId, signal) => { providerAdmission.resolve(); await releaseProviderAdmission.promise; - signal?.throwIfAborted(); + if (signal?.aborted) { + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.cancelled", + payload: { reason: "test cancellation", status: "cancelled" }, + runId, + sourceEventId: `eager-cancel.cancelled:${runId}`, + }, + ], + }); + signal.throwIfAborted(); + } sideEffects += 1; }; backend.cancelActiveTurn = async () => { @@ -314,6 +414,7 @@ describe("driver runtime boundary", () => { commandId: "eager-cancel", kind: "turn.cancel", reason: "test cancellation", + runId: DRIVER_TEST_IDS.runId, }, ]); const cancelAccepted = Promise.withResolvers(); @@ -351,6 +452,834 @@ describe("driver runtime boundary", () => { }); }); + test("rejects stale run commands before acknowledgement or side effects", async () => { + const backend = createBackend(); + const permissions = new DriverPermissionBroker(() => null); + const socket = new FakeDriverRuntimeIo( + [ + { + commandId: "stale-cancel", + kind: "turn.cancel", + reason: "must not cancel", + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: "stale-permission", + decision: "allow_once", + kind: "permission.resolve", + requestId: "active-permission", + runId: DRIVER_TEST_IDS.runId, + }, + { + argumentsJson: "{}", + commandId: "stale-mcp", + kind: "mcp.execute", + requestId: "stale-mcp-request", + runId: DRIVER_TEST_IDS.runId, + serverId: "stale-server", + toolCallId: "stale-tool-call", + toolName: "mustNotRun", + }, + { + commandId: "stale-input", + input: { text: "must not run" }, + kind: "input.start", + requestId: "stale-input-request", + runId: DRIVER_TEST_IDS.runId, + }, + ], + DRIVER_TEST_IDS.secondRunId, + ); + const permission = permissions.request(socket, { + rawInput: null, + requestId: "active-permission", + title: "Allow active run?", + toolCallId: "active-tool-call", + toolKind: "test", + }); + let mcpPreparations = 0; + const runtimeState = new DriverRuntimeStateMachine("ready"); + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => { + mcpPreparations += 1; + throw new Error("stale MCP command reached preparation"); + }, + permissionRequests: permissions, + runtimeState, + }); + + await dispatcher.run(socket, logger); + + expect(socket.updates).toHaveLength(4); + expect(socket.updates).toEqual( + expect.arrayContaining( + ["stale-cancel", "stale-permission", "stale-mcp", "stale-input"].map((commandId) => + expect.objectContaining({ + commandId, + status: "failed", + }), + ), + ), + ); + expect(socket.updates.some((update) => update.status === "accepted")).toBe(false); + expect(socket.currentRunId()).toBe(DRIVER_TEST_IDS.secondRunId); + expect(backend.cancelledReasons).toEqual([]); + expect(backend.handledInputs).toEqual([]); + expect(mcpPreparations).toBe(0); + expect(permissions.hasPending()).toBe(true); + + expect(permissions.resolve("active-permission", "reject_once")).toBe(true); + await expect(permission).resolves.toBe("reject_once"); + }); + + test("rechecks permission ownership after the accepted acknowledgement", async () => { + let currentRunId = DRIVER_TEST_IDS.runId; + class RunSwitchingSocket extends FakeDriverRuntimeIo { + override currentRunId() { + return currentRunId; + } + + override async commandUpdate( + update: Parameters[0], + signal: AbortSignal, + ) { + await super.commandUpdate(update, signal); + if (update.status === "accepted") { + currentRunId = DRIVER_TEST_IDS.secondRunId; + } + } + } + const command: RuntimeCommand = { + commandId: "permission-run-switch", + decision: "allow_once", + kind: "permission.resolve", + requestId: "permission-run-switch-request", + runId: DRIVER_TEST_IDS.runId, + }; + const socket = new RunSwitchingSocket([command], DRIVER_TEST_IDS.runId); + const permissions = new DriverPermissionBroker(() => null); + const pending = permissions.request(socket, { + rawInput: null, + requestId: command.requestId, + title: "Allow test tool?", + toolCallId: "permission-run-switch-tool", + toolKind: "test", + }); + const { dispatcher, logger } = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + permissionRequests: permissions, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await dispatcher.run(socket, logger); + + expect(socket.updates.map(({ status }) => status)).toEqual(["accepted", "failed"]); + expect(permissions.hasPending()).toBeTrue(); + expect(permissions.resolve(command.requestId, "reject_once")).toBeTrue(); + await expect(pending).resolves.toBe("reject_once"); + }); + + test.each(["session.stop"] as const)( + "rejects provider permission requests after the terminal before %s", + async (controlKind) => { + const permissionPending = Promise.withResolvers(); + const permissionSettled = Promise.withResolvers(); + const controlAccepted = Promise.withResolvers(); + const releaseControlAccepted = Promise.withResolvers(); + const backendCancelled = Promise.withResolvers(); + let cancellationRequests = 0; + let mcpPreparations = 0; + let permissionDecision: "allow_once" | "reject_once" | null = null; + let providerSignal: AbortSignal | undefined; + const permissions = new DriverPermissionBroker(() => null); + const backend = createBackend(); + backend.handleInput = async (context, _input, _runId, signal) => { + providerSignal = signal; + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId: "terminal-wins-cancel-race", + }, + ], + }); + const permission = permissions.request( + socket, + { + rawInput: null, + requestId: "terminal-first-permission", + title: "Allow terminal-first test?", + toolCallId: "terminal-first-permission-tool", + toolKind: "test", + }, + signal, + ); + permissionPending.resolve(); + permissionDecision = await permission; + permissionSettled.resolve(); + }; + backend.cancelActiveTurn = async () => { + cancellationRequests += 1; + backendCancelled.resolve(); + }; + const control: RuntimeCommand = + controlKind === "turn.cancel" + ? { + commandId: "terminal-first-control", + kind: "turn.cancel", + reason: "too late", + runId: DRIVER_TEST_IDS.runId, + } + : { + commandId: "terminal-first-control", + kind: "session.stop", + reason: "stop after terminal", + }; + const runtimeState = new DriverRuntimeStateMachine("ready"); + const socket = new FakeDriverRuntimeIo([ + { + commandId: "input-terminal-first", + input: { text: "finish" }, + kind: "input.start", + requestId: "request-terminal-first", + runId: DRIVER_TEST_IDS.runId, + }, + { + argumentsJson: "{}", + commandId: "mcp-after-terminal", + kind: "mcp.execute", + requestId: "request-mcp-after-terminal", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: "tool-mcp-after-terminal", + toolName: "createIssue", + }, + control, + ]); + const nextCommand = socket.nextCommand.bind(socket); + let reads = 0; + socket.nextCommand = async (signal) => { + reads += 1; + if (reads === 2) { + await permissionPending.promise; + } + return nextCommand(signal); + }; + const commandUpdate = socket.commandUpdate.bind(socket); + socket.commandUpdate = async (update, signal) => { + await commandUpdate(update, signal); + if (update.commandId === control.commandId && update.status === "accepted") { + controlAccepted.resolve(); + await releaseControlAccepted.promise; + } + }; + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => + socket.updates.some( + (update) => update.commandId === control.commandId && update.status === "completed", + ), + mcpPrepare: async () => { + mcpPreparations += 1; + throw new Error("MCP command crossed the run terminal fence"); + }, + permissionRequests: permissions, + runtimeState, + }); + + const run = dispatcher.run(socket, logger); + await controlAccepted.promise; + const pendingAtAcceptance = permissions.hasPending(); + releaseControlAccepted.resolve(); + await permissionSettled.promise; + + expect(providerSignal?.aborted).toBe(false); + expect(pendingAtAcceptance).toBe(false); + expect(mcpPreparations).toBe(0); + expect( + socket.updates.some( + (update) => update.commandId === "mcp-after-terminal" && update.status === "accepted", + ), + ).toBe(false); + + if (controlKind === "session.stop") { + await backendCancelled.promise; + expect(cancellationRequests).toBe(1); + expect(permissions.hasPending()).toBe(false); + } else { + expect(cancellationRequests).toBe(0); + } + await run; + + expect(permissionDecision).toBe("reject_once"); + expect(socket.updates).toContainEqual({ + commandId: "input-terminal-first", + result: { requestId: "request-terminal-first" }, + status: "completed", + }); + expect(socket.updates).toContainEqual( + expect.objectContaining({ + commandId: "mcp-after-terminal", + status: "failed", + }), + ); + expect(socket.updates.at(-1)).toEqual({ + commandId: control.commandId, + status: "completed", + }); + }, + ); + + test("closes a pending custom-backend permission before publishing the run terminal", async () => { + const resolutionPersisted = Promise.withResolvers(); + const releaseResolutionAck = Promise.withResolvers(); + const permissions = new DriverPermissionBroker(() => null); + let permissionOutcome: Promise<"allow_once" | "reject_once"> | null = null; + let requestedAttempts = 0; + const socket = new FakeDriverRuntimeIo([ + { + commandId: "input-with-pending-permission", + input: { text: "finish while permission is pending" }, + kind: "input.start", + requestId: "input-with-pending-permission-request", + runId: DRIVER_TEST_IDS.runId, + }, + ]); + const pushEvents = socket.pushEvents.bind(socket); + socket.pushEvents = async (input) => { + const result = await pushEvents(input); + if (input.events.some(({ kind }) => kind === "permission.requested")) { + requestedAttempts += 1; + if (requestedAttempts === 1) { + throw new Error("permission.requested ACK lost after persistence"); + } + } + if (input.events.some(({ kind }) => kind === "permission.resolved")) { + resolutionPersisted.resolve(); + await releaseResolutionAck.promise; + } + return result; + }; + const backend = createBackend(); + backend.handleInput = async (context, _input, runId) => { + const permission = context.ports.permission.request({ + rawInput: null, + requestId: "pending-custom-permission", + title: "Allow custom backend action?", + toolCallId: "pending-custom-permission-tool", + toolKind: "test", + }); + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + runId, + sourceEventId: `permission-barrier.completed:${runId}`, + }, + ], + }); + await permission; + }; + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => + socket.updates.some( + ({ commandId, status }) => + commandId === "input-with-pending-permission" && status === "completed", + ), + permissionRequest: (input, signal) => { + permissionOutcome = permissions.request(socket, input, signal); + return permissionOutcome; + }, + permissionRequests: permissions, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const run = dispatcher.run(socket, logger); + await resolutionPersisted.promise; + expect(permissions.hasPending()).toBe(true); + expect( + socket.pushedEvents.flatMap(({ events }) => events.map(({ kind }) => kind)), + ).not.toContain("run.completed"); + + releaseResolutionAck.resolve(); + await run; + + const kinds = socket.pushedEvents.flatMap(({ events }) => events.map(({ kind }) => kind)); + expect(requestedAttempts).toBe(2); + expect(permissionOutcome).not.toBeNull(); + await expect(permissionOutcome!).resolves.toBe("reject_once"); + expect(kinds.indexOf("permission.resolved")).toBeLessThan(kinds.indexOf("run.completed")); + expect(kinds.slice(kinds.indexOf("run.completed") + 1)).not.toContain("permission.resolved"); + }); + + test("blocks the terminal until an orphaned permission ACK lifecycle recovers", async () => { + const permissions = new DriverPermissionBroker(() => null); + const socket = new FakeDriverRuntimeIo([], DRIVER_TEST_IDS.runId); + const pushEvents = socket.pushEvents.bind(socket); + let failPermissionDelivery = true; + let permissionAttempts = 0; + socket.pushEvents = async (input) => { + if ( + failPermissionDelivery && + input.events.some(({ kind }) => kind === "permission.requested") + ) { + permissionAttempts += 1; + if (permissionAttempts === 1) { + await pushEvents(input); + } + throw new Error("permission ACK permanently unavailable"); + } + return pushEvents(input); + }; + + await expect( + permissions.request(socket, { + rawInput: null, + requestId: "orphaned-permission", + title: "Allow unavailable action?", + toolCallId: "orphaned-permission-tool", + toolKind: "test", + }), + ).rejects.toBeInstanceOf(PermissionEventDeliveryError); + expect(permissions.hasPending()).toBe(false); + socket.registerRunTerminalBarrier((events) => + events.some(({ kind }) => kind === "run.completed") + ? permissions.rejectRunAndWait(DRIVER_TEST_IDS.runId) + : undefined, + ); + + await expect( + socket.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + runId: DRIVER_TEST_IDS.runId, + sourceEventId: "blocked-by-permission-failure", + }, + ], + }), + ).rejects.toBeInstanceOf(PermissionEventDeliveryError); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toBeNull(); + failPermissionDelivery = false; + await expect( + socket.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + runId: DRIVER_TEST_IDS.runId, + sourceEventId: "blocked-by-permission-failure", + }, + ], + }), + ).resolves.toMatchObject({ accepted: [{ type: "run.completed" }] }); + expect(socket.pushedEvents.flatMap(({ events }) => events.map(({ kind }) => kind))).toEqual([ + "permission.requested", + "permission.requested", + "permission.resolved", + "diagnostic.reported", + "run.completed", + ]); + }); + + test("waits for an admitted MCP command before publishing the run terminal", async () => { + const acceptedEntered = Promise.withResolvers(); + const releaseAccepted = Promise.withResolvers(); + const executionEntered = Promise.withResolvers(); + const releaseExecution = Promise.withResolvers(); + const terminalEntered = Promise.withResolvers(); + const commands: RuntimeCommand[] = [ + { + commandId: "input-with-mcp-barrier", + input: { text: "finish after MCP" }, + kind: "input.start", + requestId: "input-with-mcp-barrier-request", + runId: DRIVER_TEST_IDS.runId, + }, + { + argumentsJson: "{}", + commandId: "mcp-before-terminal-fence", + kind: "mcp.execute", + requestId: "mcp-before-terminal-fence-request", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: "tool-before-terminal-fence", + toolName: "createIssue", + }, + { + argumentsJson: "{}", + commandId: "mcp-after-terminal-fence", + kind: "mcp.execute", + requestId: "mcp-after-terminal-fence-request", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: "tool-after-terminal-fence", + toolName: "updateIssue", + }, + ]; + const socket = new FakeDriverRuntimeIo(commands); + const order: string[] = []; + const pushedEvents = socket.pushEvents.bind(socket); + socket.pushEvents = async (input) => { + const result = await pushedEvents(input); + order.push(...input.events.map((event) => `event:${event.kind}`)); + return result; + }; + const commandUpdate = socket.commandUpdate.bind(socket); + socket.commandUpdate = async (update, signal) => { + await commandUpdate(update, signal); + order.push(`command:${update.commandId}:${update.status}`); + if (update.commandId === "mcp-before-terminal-fence" && update.status === "accepted") { + acceptedEntered.resolve(); + await releaseAccepted.promise; + } + }; + const backend = createBackend(); + backend.handleInput = async (_context, _input, runId) => { + await acceptedEntered.promise; + terminalEntered.resolve(); + await socket.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `mcp-barrier.completed:${runId}`, + }, + ], + }); + }; + const preparedCommands: string[] = []; + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => + socket.updates.some( + (update) => + update.commandId === "input-with-mcp-barrier" && update.status === "completed", + ), + mcpPrepare: async (command) => { + preparedCommands.push(command.commandId); + return { + execute: async () => { + executionEntered.resolve(); + await releaseExecution.promise; + return { + outputText: "created", + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }; + }, + async [Symbol.asyncDispose]() {}, + }; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const run = dispatcher.run(socket, logger); + await terminalEntered.promise; + await Bun.sleep(0); + expect(order).not.toContain("event:run.completed"); + expect(preparedCommands).toEqual([]); + + releaseAccepted.resolve(); + await executionEntered.promise; + await waitForUpdate( + socket, + (update) => update.commandId === "mcp-after-terminal-fence" && update.status === "failed", + ); + expect( + socket.updates.some( + (update) => update.commandId === "mcp-after-terminal-fence" && update.status === "accepted", + ), + ).toBe(false); + expect(preparedCommands).toEqual(["mcp-before-terminal-fence"]); + expect(order).not.toContain("event:run.completed"); + + releaseExecution.resolve(); + await expect( + settlePromiseWithTimeout(run, { + label: "same-run MCP terminal barrier", + timeoutMs: 1_500, + }), + ).resolves.toMatchObject({ status: "completed" }); + + const mcpCompleted = order.indexOf("command:mcp-before-terminal-fence:completed"); + const runCompleted = order.indexOf("event:run.completed"); + expect(mcpCompleted).toBeGreaterThan(-1); + expect(runCompleted).toBeGreaterThan(mcpCompleted); + }); + + test("publishes run.failed without self-waiting after an MCP task fails", async () => { + const executionEntered = Promise.withResolvers(); + const releaseExecution = Promise.withResolvers(); + const terminalEntered = Promise.withResolvers(); + const shutdown = new AbortController(); + const socket = new FakeDriverRuntimeIo([ + { + commandId: "input-with-failed-mcp", + input: { text: "fail after MCP" }, + kind: "input.start", + requestId: "input-with-failed-mcp-request", + runId: DRIVER_TEST_IDS.runId, + }, + { + argumentsJson: "{}", + commandId: "mcp-terminal-delivery-failure", + kind: "mcp.execute", + requestId: "mcp-terminal-delivery-failure-request", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: "tool-terminal-delivery-failure", + toolName: "createIssue", + }, + ]); + const pushEvents = socket.pushEvents.bind(socket); + socket.pushEvents = async (input) => { + if ( + input.events.some( + (event) => + event.kind === "tool.call.updated" && + event.sourceEventId === "mcp.execute.completed:mcp-terminal-delivery-failure", + ) + ) { + throw new Error("MCP terminal event unavailable"); + } + return pushEvents(input); + }; + const backend = createBackend(); + backend.handleInput = async (context, _input, runId) => { + await executionEntered.promise; + terminalEntered.resolve(); + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `failed-mcp.completed:${runId}`, + }, + ], + }); + }; + let rememberedFailure: Parameters[0] | null = null; + let shutdownTask: Promise | null = null; + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => shutdown.signal.aborted, + mcpPrepare: async (command) => ({ + execute: async () => { + executionEntered.resolve(); + await releaseExecution.promise; + return { + outputText: "created", + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }; + }, + async [Symbol.asyncDispose]() {}, + }), + rememberRunFailure: (error) => { + rememberedFailure ??= structuredClone(error); + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + shutdown: async () => { + shutdownTask ??= (async () => { + const error = rememberedFailure ?? { + code: "driver.mcp_task_failed", + details: {}, + message: "Driver MCP task failed.", + retryable: false, + }; + await socket.pushEvents({ + events: [ + { + kind: "run.failed", + payload: { error, recoverable: error.retryable, status: "failed" }, + runId: DRIVER_TEST_IDS.runId, + sourceEventId: `failed-mcp.failed:${DRIVER_TEST_IDS.runId}`, + }, + ], + }); + shutdown.abort(new Error("driver.mcp_task_failed")); + })(); + await shutdownTask; + }, + shutdownSignal: shutdown.signal, + }); + + const run = dispatcher.run(socket, logger); + await terminalEntered.promise; + releaseExecution.resolve(); + const outcome = await settlePromiseWithTimeout(run, { + label: "failed MCP run terminal", + timeoutMs: 1_500, + }); + + expect(outcome.status).toBe("completed"); + expect( + socket.pushedEvents + .flatMap(({ events }) => events) + .filter((event) => event.kind === "run.failed"), + ).toHaveLength(1); + expect( + socket.pushedEvents + .flatMap(({ events }) => events) + .some((event) => event.kind === "run.completed"), + ).toBe(false); + expect(socket.updates).toContainEqual({ + commandId: "mcp-terminal-delivery-failure", + status: "accepted", + }); + expect( + socket.updates.some( + (update) => + update.commandId === "mcp-terminal-delivery-failure" && update.status !== "accepted", + ), + ).toBe(false); + }); + + test.each([ + { + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId: DRIVER_TEST_IDS.runId, + sourceEventId: "multiple-terminal.completed", + }, + { + kind: "run.failed", + payload: { + error: { code: "test", details: {}, message: "failed", retryable: false }, + recoverable: false, + status: "failed", + }, + runId: DRIVER_TEST_IDS.runId, + sourceEventId: "multiple-terminal.failed", + }, + ], + message: "cannot contain multiple run terminals", + name: "multiple terminals", + }, + { + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId: DRIVER_TEST_IDS.runId, + sourceEventId: "non-final-terminal.completed", + }, + { + kind: "diagnostic.reported", + payload: { message: "must not cross terminal" }, + runId: DRIVER_TEST_IDS.runId, + sourceEventId: "non-final-terminal.diagnostic", + }, + ], + message: "must be the only event", + name: "an event after the terminal", + }, + ] as const)("rejects $name before event delivery", async ({ events, message }) => { + const socket = new FakeDriverRuntimeIo([ + { + commandId: `input-${events[0].sourceEventId}`, + input: { text: "validate terminal batch" }, + kind: "input.start", + requestId: `request-${events[0].sourceEventId}`, + runId: DRIVER_TEST_IDS.runId, + }, + ]); + let rejection: unknown; + const backend = createBackend(); + backend.handleInput = async (_context, _input, runId) => { + try { + await socket.pushEvents({ events: structuredClone(events) }); + } catch (error) { + rejection = error; + } + await socket.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `valid-terminal:${runId}`, + }, + ], + }); + }; + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => socket.isDrained(), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await dispatcher.run(socket, logger); + + expect(rejection).toBeInstanceOf(Error); + expect((rejection as Error).message).toContain(message); + expect(socket.pushedEvents).toHaveLength(1); + expect(socket.pushedEvents[0]?.events[0]?.sourceEventId).toBe( + `valid-terminal:${DRIVER_TEST_IDS.runId}`, + ); + }); + + test("keeps an ordinary late provider rejection failed after cancellation wins", async () => { + const inputEntered = Promise.withResolvers(); + const rejectInput = Promise.withResolvers(); + const backend = createBackend(); + backend.handleInput = async () => { + inputEntered.resolve(); + await rejectInput.promise; + throw new Error("ordinary late rejection"); + }; + backend.cancelActiveTurn = async () => { + await inputEntered.promise; + rejectInput.resolve(); + }; + const socket = new FakeDriverRuntimeIo([ + { + commandId: "input-late-reject", + input: { text: "wait" }, + kind: "input.start", + requestId: "request-late-reject", + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: "cancel-before-reject", + kind: "turn.cancel", + reason: "cancel", + runId: DRIVER_TEST_IDS.runId, + }, + ]); + const runtimeState = new DriverRuntimeStateMachine("ready"); + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => socket.isDrained(), + runtimeState, + }); + + await dispatcher.run(socket, logger); + + expect(socket.updates).toContainEqual( + expect.objectContaining({ commandId: "input-late-reject", status: "failed" }), + ); + expect(socket.updates).not.toContainEqual({ + commandId: "input-late-reject", + status: "cancelled", + }); + expect(runtimeState.status()).toBe("failed"); + }); + test("does not report an input failure before a rejected shutdown barrier", async () => { const backend = createBackend(); backend.failInput = true; @@ -552,6 +1481,7 @@ describe("driver runtime boundary", () => { commandId: "mcp-terminal-failure", kind: "mcp.execute", requestId: "request-terminal-failure", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: "tool-terminal-failure", toolName: "createIssue", @@ -648,8 +1578,18 @@ describe("driver runtime boundary", () => { async (kind) => { const backend = createBackend(); let sideEffects = 0; - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId) => { sideEffects += 1; + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `slow-terminal.completed:${runId}`, + }, + ], + }); }; const first: RuntimeCommand = kind === "input" @@ -665,6 +1605,7 @@ describe("driver runtime boundary", () => { commandId: "ack-blocked-mcp", kind: "mcp.execute", requestId: "ack-blocked-request", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: "tool-ack-blocked", toolName: "createIssue", @@ -683,7 +1624,10 @@ describe("driver runtime boundary", () => { kind: "session.stop", reason: "test.stop", }; - const socket = new FakeDriverRuntimeIo([first, next]); + const socket = new FakeDriverRuntimeIo( + [first, next], + kind === "mcp" ? DRIVER_TEST_IDS.runId : undefined, + ); const recordUpdate = socket.commandUpdate.bind(socket); let terminalAttempts = 0; socket.commandUpdate = async (update, signal) => { @@ -743,9 +1687,10 @@ describe("driver runtime boundary", () => { commandId: `cancel-${index}`, kind: "turn.cancel", reason: `reason-${index}`, + runId: DRIVER_TEST_IDS.runId, })); commands.push(structuredClone(commands[0]!)); - const socket = new FakeDriverRuntimeIo(commands); + const socket = new FakeDriverRuntimeIo(commands, DRIVER_TEST_IDS.runId); const runtimeState = new DriverRuntimeStateMachine("ready"); const { dispatcher, logger } = createDispatcher({ backend, diff --git a/tests/driver-runtime-boundary-event-envelope.test.ts b/tests/driver-runtime-boundary-event-envelope.test.ts index d61ead6..df158a4 100644 --- a/tests/driver-runtime-boundary-event-envelope.test.ts +++ b/tests/driver-runtime-boundary-event-envelope.test.ts @@ -20,9 +20,19 @@ describe("driver runtime boundary", () => { const releaseAccepted = Promise.withResolvers(); let sideEffects = 0; let terminalStarted = false; - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId) => { sideEffects += 1; await releaseEffect.promise; + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `serialized-replay.completed:${runId}`, + }, + ], + }); }; const command: RuntimeCommand = kind === "input" @@ -38,11 +48,15 @@ describe("driver runtime boundary", () => { commandId: "serialized-mcp-replay", kind: "mcp.execute", requestId: "serialized-request", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: "tool-serialized", toolName: "createIssue", }; - const socket = new FakeDriverRuntimeIo([command, structuredClone(command)]); + const socket = new FakeDriverRuntimeIo( + [command, structuredClone(command)], + kind === "mcp" ? DRIVER_TEST_IDS.runId : undefined, + ); const recordUpdate = socket.commandUpdate.bind(socket); let accepted = 0; socket.commandUpdate = async (update, signal) => { @@ -99,8 +113,18 @@ describe("driver runtime boundary", () => { async (kind) => { const backend = createBackend(); let sideEffects = 0; - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId) => { sideEffects += 1; + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `joined-replay.completed:${runId}`, + }, + ], + }); }; const command: RuntimeCommand = kind === "input" @@ -116,11 +140,15 @@ describe("driver runtime boundary", () => { commandId: "joined-mcp-replay", kind: "mcp.execute", requestId: "joined-request", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: "tool-joined", toolName: "createIssue", }; - const socket = new FakeDriverRuntimeIo([command, structuredClone(command)]); + const socket = new FakeDriverRuntimeIo( + [command, structuredClone(command)], + kind === "mcp" ? DRIVER_TEST_IDS.runId : undefined, + ); const terminalEntered = Promise.withResolvers(); const releaseTerminal = Promise.withResolvers(); const nextCommand = socket.nextCommand.bind(socket); @@ -189,8 +217,18 @@ describe("driver runtime boundary", () => { async (kind) => { const backend = createBackend(); let sideEffects = 0; - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId) => { sideEffects += 1; + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `failed-joined-replay.completed:${runId}`, + }, + ], + }); }; const command: RuntimeCommand = kind === "input" @@ -206,11 +244,15 @@ describe("driver runtime boundary", () => { commandId: "failed-joined-mcp-replay", kind: "mcp.execute", requestId: "failed-joined-request", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: "tool-failed-joined", toolName: "createIssue", }; - const socket = new FakeDriverRuntimeIo([command, structuredClone(command)]); + const socket = new FakeDriverRuntimeIo( + [command, structuredClone(command)], + kind === "mcp" ? DRIVER_TEST_IDS.runId : undefined, + ); const terminalEntered = Promise.withResolvers(); const releaseTerminal = Promise.withResolvers(); const nextCommand = socket.nextCommand.bind(socket); @@ -296,11 +338,15 @@ describe("driver runtime boundary", () => { commandId: `sink-mutation-${terminalStatus}`, kind: "mcp.execute", requestId: "sink-mutation-request", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: "tool-sink-mutation", toolName: "createIssue", }; - const socket = new FakeDriverRuntimeIo([command, structuredClone(command)]); + const socket = new FakeDriverRuntimeIo( + [command, structuredClone(command)], + DRIVER_TEST_IDS.runId, + ); const terminalEntered = Promise.withResolvers(); const releaseTerminal = Promise.withResolvers(); const nextCommand = socket.nextCommand.bind(socket); @@ -323,23 +369,23 @@ describe("driver runtime boundary", () => { terminalSnapshots.push(structuredClone(update)); if (terminalSnapshots.length === 1) { - if (update.result !== undefined && update.result !== null) { + if (update.status === "completed" && update.result !== undefined) { Reflect.set(update.result, "outputText", "mutated synchronously"); } - if (update.error !== undefined) { + if (update.status === "failed") { Reflect.set(update.error, "message", "mutated synchronously"); } terminalEntered.resolve(); await releaseTerminal.promise; const debug = - update.result === undefined || update.result === null + update.status !== "completed" || update.result === undefined ? undefined : (Reflect.get(update.result, "debug") as { nested?: string } | undefined); if (debug !== undefined) { debug.nested = "mutated after await"; } - if (update.error !== undefined) { + if (update.status === "failed") { Reflect.set(update.error.details, "commandId", "mutated after await"); } } @@ -387,8 +433,18 @@ describe("driver runtime boundary", () => { async (kind) => { const backend = createBackend(); let sideEffects = 0; - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId) => { sideEffects += 1; + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `cached-replay.completed:${runId}`, + }, + ], + }); }; const runtimeState = new DriverRuntimeStateMachine("ready"); const command: RuntimeCommand = @@ -405,11 +461,15 @@ describe("driver runtime boundary", () => { commandId: "mcp-report-failure", kind: "mcp.execute", requestId: "request-report-failure", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: "tool-report-failure", toolName: "createIssue", }; - const socket = new FakeDriverRuntimeIo([command]); + const socket = new FakeDriverRuntimeIo( + [command], + kind === "mcp" ? DRIVER_TEST_IDS.runId : undefined, + ); const recordUpdate = socket.commandUpdate.bind(socket); const terminalAttempts: string[] = []; socket.commandUpdate = async (update, signal) => { diff --git a/tests/driver-runtime-boundary-fixtures.ts b/tests/driver-runtime-boundary-fixtures.ts index b452113..39227f9 100644 --- a/tests/driver-runtime-boundary-fixtures.ts +++ b/tests/driver-runtime-boundary-fixtures.ts @@ -2,11 +2,21 @@ import type { AgentDriverBackend, AgentDriverContext } from "../src/core/agent-d import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { DriverCommandDispatcher } from "../src/core/driver-command-dispatcher"; import { DriverPermissionBroker } from "../src/core/driver-permission-broker"; -import type { DriverRuntimeIo } from "../src/core/driver-runtime-io"; +import { + assertIsolatedRunTerminalBatch, + withSourceEventIds, + type DriverRuntimeIo, +} from "../src/core/driver-runtime-io"; +import type { DriverRunTerminalBarrier } from "../src/core/driver-runtime-io"; import type { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; +import { + DriverTerminalStateMachine, + type DriverRunTicket, +} from "../src/core/driver-terminal-state"; import type { AgentDriverMcpPort } from "../src/host-ports"; import { createDisabledLogger } from "../src/observability"; import { createDriverStartInputFromBootPayload } from "../src/protocol/start"; +import type { RunId } from "../src/protocol/id"; import type { McpExecuteCommand, McpExternalToolEffectExecution, @@ -25,27 +35,53 @@ export class FakeDriverRuntimeIo implements DriverRuntimeIo { readonly pushedEvents: Parameters[0][] = []; readonly updates: Parameters[0][] = []; readonly #commands: readonly RuntimeCommand[]; - #activeRunId: ReturnType = null; + #activeRunTicket: DriverRunTicket | null = null; #commandIndex = 0; + #runTerminalBarrier: DriverRunTerminalBarrier | null = null; + readonly #terminalState = new DriverTerminalStateMachine(); - constructor(commands: readonly RuntimeCommand[]) { + constructor(commands: readonly RuntimeCommand[], activeRunId?: RunId) { this.#commands = commands; + if (activeRunId !== undefined) { + this.beginRun(activeRunId); + } + } + + beginRun(runId: Parameters[0]): DriverRunTicket { + const ticket = this.#terminalState.beginRun(runId); + this.#activeRunTicket = ticket; + return ticket; } - beginRun(runId: Parameters[0]): void { - this.#activeRunId = runId; + claimRunCancellation( + ticket: DriverRunTicket, + reason: string, + ): ReturnType { + return this.#terminalState.claimCancellation(ticket, reason); } currentRunId(): ReturnType { - return this.#activeRunId; + return this.#terminalState.currentRunId(); } - endRun(runId: Parameters[0]): void { - if (this.#activeRunId === runId) { - this.#activeRunId = null; + releaseRun(ticket: DriverRunTicket, reason: "command_acked" | "driver_failing"): void { + this.#terminalState.releaseRun(ticket, reason); + if (this.#activeRunTicket === ticket) { + this.#activeRunTicket = null; } } + runSnapshot(runId?: Parameters[0]) { + return this.#terminalState.snapshotRun(runId); + } + + settleRunInput( + ticket: DriverRunTicket, + outcome: Parameters[1], + ): ReturnType { + return this.#terminalState.settleInput(ticket, outcome); + } + async heartbeat(): ReturnType { return { heartbeatCount: 1, @@ -71,10 +107,17 @@ export class FakeDriverRuntimeIo implements DriverRuntimeIo { attempt: 1, effectId: `test-effect-${input.commandId}`, idempotencyKey: `test-effect-${input.commandId}`, - kind: "execute", + kind: "claimed", }; } + async observeExternalToolEffect( + input: Parameters[0], + _signal: AbortSignal, + ): ReturnType { + return { effectId: `test-effect-${input.commandId}`, kind: "intent" }; + } + isDrained(): boolean { return this.#commandIndex >= this.#commands.length; } @@ -87,40 +130,107 @@ export class FakeDriverRuntimeIo implements DriverRuntimeIo { } async completeRun(_signal?: AbortSignal): Promise { + const runId = this.#terminalState.terminalRunId(DRIVER_TEST_IDS.runId); + if (runId === null) { + throw new Error("Driver run terminal requires an exact run ID."); + } + const terminal = { runId, status: "completed" } as const; + if (this.#terminalState.selectInstanceTerminal(terminal) === "acked") { + return; + } this.completedRunReasons.push("completed"); + this.#terminalState.ackInstanceTerminal(terminal); } - async completeExternalToolEffect( - _input: Parameters[0], + async settleExternalToolEffect( + input: Parameters[0], _signal: AbortSignal, - ): Promise { - return; + ): ReturnType { + return input.settlement.kind === "succeeded" + ? { + effectId: input.effectId, + kind: "succeeded", + result: structuredClone(input.settlement.result), + } + : { effectId: input.effectId, kind: "unknown" }; } async failRun( error: Parameters[0], _signal?: AbortSignal, ): Promise { + const runId = this.#terminalState.terminalRunId(DRIVER_TEST_IDS.runId); + if (runId === null) { + throw new Error("Driver run terminal requires an exact run ID."); + } + const terminal = { error, runId, status: "failed" } as const; + if (this.#terminalState.selectInstanceTerminal(terminal) === "acked") { + return; + } this.failedRuns.push(error); + this.#terminalState.ackInstanceTerminal(terminal); } async pushEvents( input: Parameters[0], ): ReturnType { - this.pushedEvents.push(input); + input.signal?.throwIfAborted(); + const events = structuredClone(withSourceEventIds(input.events)); + assertIsolatedRunTerminalBatch(events); + const barrier = this.#runTerminalBarrier; + if (barrier !== null) { + const pending = barrier(events); + if (pending !== undefined) { + await pending; + } + } + this.pushedEvents.push({ ...input, events }); + const ticket = this.#activeRunTicket; + const terminalEvent = events.find( + (event) => + event.kind === "run.cancelled" || + event.kind === "run.completed" || + event.kind === "run.failed", + ); + if (terminalEvent !== undefined && ticket !== null) { + const status = terminalEvent.kind.slice("run.".length) as + | "cancelled" + | "completed" + | "failed"; + const selected = this.#terminalState.selectRunTerminal(ticket, { + event: terminalEvent, + runId: ticket.runId, + sourceEventId: terminalEvent.sourceEventId!, + status, + }); + if (selected === "cancelled") { + throw new Error("Driver completed terminal lost the cancellation race."); + } + } + const accepted = events.map((event, index) => ({ + eventId: event.sourceEventId!, + seq: index + 1, + type: event.kind, + })); + if (terminalEvent !== undefined && ticket !== null) { + this.#terminalState.ackRunTerminal(ticket, accepted.at(-1)!); + } return { - accepted: input.events.map((event, index) => ({ - seq: index + 1, - type: event.kind, - })), + accepted, }; } - async markExternalToolEffectUnknown( - _input: Parameters[0], - _signal: AbortSignal, - ): Promise { - return; + registerRunTerminalBarrier(barrier: DriverRunTerminalBarrier): () => void { + if (this.#runTerminalBarrier !== null) { + throw new Error("Driver run terminal barrier is already registered."); + } + + this.#runTerminalBarrier = barrier; + return () => { + if (this.#runTerminalBarrier === barrier) { + this.#runTerminalBarrier = null; + } + }; } } @@ -139,12 +249,22 @@ export function createBackend(): RecordingBackend { async cancelActiveTurn(_context, reason) { this.cancelledReasons.push(reason); }, - async handleInput(context) { + async handleInput(context, _input, runId) { if (this.failInput) { throw new Error("backend rejected input"); } this.handledInputs.push(context.payload.execution.session); + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `test.run.completed:${runId}`, + }, + ], + }); }, async start(_context, signal) { signal.throwIfAborted(); @@ -155,15 +275,34 @@ export function createBackend(): RecordingBackend { }; } +export async function settleBackendInput( + context: AgentDriverContext, + runId: RunId, + signal?: AbortSignal, +): Promise { + const status = signal?.aborted ? "cancelled" : "completed"; + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: `run.${status}`, + payload: { status }, + runId, + sourceEventId: `test.run.${status}:${runId}`, + }, + ], + }); + signal?.throwIfAborted(); +} + export function createDispatcher(input: { backend: AgentDriverBackend; isShuttingDown?: () => boolean; mcpExecute?: ( command: McpExecuteCommand, - signal: AbortSignal, effect: McpExternalToolEffectExecution, ) => Promise; mcpPrepare?: AgentDriverMcpPort["prepare"]; + permissionRequest?: AgentDriverContext["ports"]["permission"]["request"]; permissionRequests?: DriverPermissionBroker; rememberRunFailure?: (error: Parameters[0]) => void; runtimeState: DriverRuntimeStateMachine; @@ -196,7 +335,7 @@ export function createDispatcher(input: { logger: runtimeLogger, payload: bootPayload, permission: { - request: async () => "reject_once", + request: input.permissionRequest ?? (async () => "reject_once"), }, ports: { commandSource: { @@ -208,7 +347,7 @@ export function createDispatcher(input: { mcp: { prepare: input.mcpPrepare ?? - (async (command, signal) => ({ + (async (command) => ({ execute: (effect) => ( input.mcpExecute ?? @@ -218,7 +357,7 @@ export function createDispatcher(input: { serverId: command.serverId, toolName: command.toolName, })) - )(command, signal, effect), + )(command, effect), async [Symbol.asyncDispose]() {}, })), }, diff --git a/tests/driver-runtime-boundary-state.test.ts b/tests/driver-runtime-boundary-state.test.ts index b8f0096..dcb7a47 100644 --- a/tests/driver-runtime-boundary-state.test.ts +++ b/tests/driver-runtime-boundary-state.test.ts @@ -3,57 +3,18 @@ import { describe, expect, test } from "bun:test"; import { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; import { createTimingEvent } from "../src/core/driver-runtime-timing"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; -import type { CredentialId, McpServerId } from "../src/protocol/boot"; import { parseDriverEventEnvelope } from "../src/protocol/events"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; -import type { DriverStartInput } from "../src/protocol/start"; -import type { McpExecuteCommand, RuntimeCommand } from "../src/runtime-command"; -import { prepareRemoteHttpMcpCommand } from "../src/runtimes/mcp/remote-http-mcp-executor"; +import type { RuntimeCommand } from "../src/runtime-command"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; import { FakeDriverRuntimeIo, - bootPayload, createBackend, createDispatcher, waitForUpdate, } from "./driver-runtime-boundary-fixtures"; -const MCP_PREFLIGHT_SERVER_ID = "01J00000000000000000000020" as McpServerId; -const MCP_PREFLIGHT_CREDENTIAL_ID = "01J00000000000000000000021" as CredentialId; - -function withMcpServers( - mcpServers: DriverStartInput["execution"]["session"]["mcpServers"], -): DriverStartInput { - return { - ...bootPayload, - execution: { - ...bootPayload.execution, - session: { - ...bootPayload.execution.session, - mcpServers, - }, - }, - }; -} - -class EffectLedgerRecordingSocket extends FakeDriverRuntimeIo { - claimCount = 0; - unknownCount = 0; - - override async claimExternalToolEffect( - input: Parameters[0], - signal: AbortSignal, - ): ReturnType { - this.claimCount += 1; - return super.claimExternalToolEffect(input, signal); - } - - override async markExternalToolEffectUnknown(): Promise { - this.unknownCount += 1; - } -} - describe("driver runtime boundary", () => { test("rejects an invalid runtime transition", () => { const state = new DriverRuntimeStateMachine("created"); @@ -356,10 +317,20 @@ describe("driver runtime boundary", () => { const release = Promise.withResolvers(); const backend = createBackend(); let calls = 0; - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId) => { calls += 1; started.resolve(); await release.promise; + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `active-replay.completed:${runId}`, + }, + ], + }); }; const command: RuntimeCommand = kind === "input" @@ -375,11 +346,15 @@ describe("driver runtime boundary", () => { commandId: "active-replay", kind: "mcp.execute", requestId: "request-replay", + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: "tool-replay", toolName: "createIssue", }; - const socket = new FakeDriverRuntimeIo([command, structuredClone(command)]); + const socket = new FakeDriverRuntimeIo( + [command, structuredClone(command)], + kind === "mcp" ? DRIVER_TEST_IDS.runId : undefined, + ); const runtimeState = new DriverRuntimeStateMachine("ready"); const { dispatcher, logger } = createDispatcher({ backend, @@ -424,790 +399,15 @@ describe("driver runtime boundary", () => { }, ); - test("does not replay an MCP effect after terminal receipt delivery is lost", async () => { - const command: RuntimeCommand = { - argumentsJson: '{"title":"once"}', - commandId: "persistent-effect", - kind: "mcp.execute", - requestId: "request-persistent-effect", - serverId: "mcp-linear", - toolCallId: "tool-persistent-effect", - toolName: "createIssue", - }; - let providerCalls = 0; - let effectResult: { - outputText: string; - requestId: string; - serverId: string; - toolName: string; - } | null = null; - const effectPersisted = Promise.withResolvers(); - - class LossySocket extends FakeDriverRuntimeIo { - override async claimExternalToolEffect(): Promise< - | { attempt: number; effectId: string; idempotencyKey: string; kind: "execute" } - | { effectId: string; kind: "completed"; result: NonNullable } - > { - return effectResult === null - ? { attempt: 1, effectId: "effect-1", idempotencyKey: "effect-1", kind: "execute" } - : { effectId: "effect-1", kind: "completed", result: effectResult }; - } - - override async completeExternalToolEffect(input: { - commandId: string; - result: NonNullable; - }): Promise { - effectResult = input.result; - effectPersisted.resolve(); - } - - override async commandUpdate( - input: Parameters[0], - signal: AbortSignal, - ): Promise { - if (input.status === "completed") { - throw new Error("control connection lost after effect receipt persisted"); - } - - await super.commandUpdate(input, signal); - } - } - - const firstSocket = new LossySocket([command]); - const first = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => firstSocket.isDrained(), - mcpExecute: async (request) => { - providerCalls += 1; - return { - outputText: "created A-1", - requestId: request.requestId, - serverId: request.serverId, - toolName: request.toolName, - }; - }, - runtimeState: new DriverRuntimeStateMachine("ready"), - }); - - await first.dispatcher.run(firstSocket, first.logger); - await effectPersisted.promise; - - const secondSocket = new FakeDriverRuntimeIo([structuredClone(command)]); - let secondClaims = 0; - let secondPreparations = 0; - const second = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => secondSocket.isDrained(), - mcpPrepare: async () => { - secondPreparations += 1; - return { - async execute() { - throw new Error("completed effect must not execute"); - }, - async [Symbol.asyncDispose]() {}, - }; - }, - runtimeState: new DriverRuntimeStateMachine("ready"), - }); - - // The default fixture ledger has no cross-process state, so carry the - // persisted effect receipt through the fresh Dispatcher explicitly. - secondSocket.claimExternalToolEffect = async () => { - secondClaims += 1; - return { - effectId: "effect-1", - kind: "completed" as const, - result: effectResult!, - }; - }; - await second.dispatcher.run(secondSocket, second.logger); - - expect(providerCalls).toBe(1); - expect(secondPreparations).toBe(1); - expect(secondClaims).toBe(1); - expect(secondSocket.updates).toEqual([ - { commandId: command.commandId, status: "accepted" }, - { commandId: command.commandId, result: effectResult, status: "completed" }, - ]); - expect(secondSocket.pushedEvents).toMatchObject([ - { - events: [{ kind: "tool.call.updated", payload: { toolCallId: command.toolCallId } }], - }, - { - events: [{ kind: "tool.call.updated", payload: { toolCallId: command.toolCallId } }], - }, - ]); - }); - - test("blocks an unknown MCP effect without another provider call", async () => { - const command: RuntimeCommand = { - argumentsJson: '{"title":"do not duplicate"}', - commandId: "unknown-effect-command", - kind: "mcp.execute", - requestId: "unknown-effect-request", - serverId: "mcp-linear", - toolCallId: "tool-unknown-effect", - toolName: "createIssue", - }; - const socket = new FakeDriverRuntimeIo([command]); - let claims = 0; - let executions = 0; - let preparations = 0; - socket.claimExternalToolEffect = async () => { - claims += 1; - return { - effectId: "01J0000000000000000000000Z", - kind: "unknown" as const, - }; - }; - const runtime = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => socket.isDrained(), - mcpPrepare: async () => { - preparations += 1; - return { - async execute() { - executions += 1; - throw new Error("unknown effect must not execute"); - }, - async [Symbol.asyncDispose]() {}, - }; - }, - runtimeState: new DriverRuntimeStateMachine("ready"), - }); - - await runtime.dispatcher.run(socket, runtime.logger); - await waitForUpdate( - socket, - (update) => update.commandId === command.commandId && update.status === "failed", - ); - - expect(preparations).toBe(1); - expect(claims).toBe(1); - expect(executions).toBe(0); - expect(socket.updates).toMatchObject([ - { commandId: command.commandId, status: "accepted" }, - { - commandId: command.commandId, - error: { - code: "driver.command_failed.mcp.execute", - message: expect.stringContaining("01J0000000000000000000000Z"), - retryable: false, - }, - status: "failed", - }, - ]); - }); - - test("disposes MCP preparation when the claim CAS observes a completed effect", async () => { - const command: McpExecuteCommand = { - argumentsJson: "{}", - commandId: "claim-race-completed", - kind: "mcp.execute", - requestId: "claim-race-request", - serverId: MCP_PREFLIGHT_SERVER_ID, - toolCallId: "claim-race-tool", - toolName: "lookup", - }; - const result = { - outputText: "already completed", - requestId: command.requestId, - serverId: command.serverId, - toolName: command.toolName, - }; - const socket = new FakeDriverRuntimeIo([command]); - socket.claimExternalToolEffect = async () => ({ - effectId: "claim-race-effect", - kind: "completed" as const, - result, - }); - let disposals = 0; - let executions = 0; - const disposed = Promise.withResolvers(); - const runtime = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => socket.isDrained(), - mcpPrepare: async () => ({ - async execute() { - executions += 1; - throw new Error("completed claim must not execute"); - }, - async [Symbol.asyncDispose]() { - disposals += 1; - disposed.resolve(); - }, - }), - runtimeState: new DriverRuntimeStateMachine("ready"), - }); - - await runtime.dispatcher.run(socket, runtime.logger); - await disposed.promise; - - expect(executions).toBe(0); - expect(disposals).toBe(1); - expect(socket.updates).toContainEqual({ - commandId: command.commandId, - result, - status: "completed", - }); - }); - - test.each(["executed", "completed claim"] as const)( - "keeps a durable %s MCP result completed when its event publication fails", - async (settlement) => { - const command: McpExecuteCommand = { - argumentsJson: "{}", - commandId: `durable-event-failure-${settlement.replaceAll(" ", "-")}`, - kind: "mcp.execute", - requestId: "durable-event-failure-request", - serverId: MCP_PREFLIGHT_SERVER_ID, - toolCallId: "durable-event-failure-tool", - toolName: "lookup", - }; - const result = { - outputText: "durable result", - requestId: command.requestId, - serverId: command.serverId, - toolName: command.toolName, - }; - const durable = Promise.withResolvers(); - const completedEventEntered = Promise.withResolvers(); - const releaseCompletedEvent = Promise.withResolvers(); - - class FailingCompletedEventSocket extends FakeDriverRuntimeIo { - unknownCount = 0; - - override async claimExternalToolEffect(): ReturnType< - FakeDriverRuntimeIo["claimExternalToolEffect"] - > { - if (settlement === "completed claim") { - durable.resolve(); - return { effectId: "durable-effect", kind: "completed", result }; - } - return { - attempt: 1, - effectId: "durable-effect", - idempotencyKey: "durable-effect", - kind: "execute", - }; - } - - override async completeExternalToolEffect(): Promise { - durable.resolve(); - } - - override async markExternalToolEffectUnknown(): Promise { - this.unknownCount += 1; - } - - override async pushEvents( - input: Parameters[0], - ): ReturnType { - if ( - input.events.some( - (event) => - event.kind === "tool.call.updated" && - typeof event.payload === "object" && - event.payload !== null && - "status" in event.payload && - event.payload.status === "completed", - ) - ) { - completedEventEntered.resolve(); - await releaseCompletedEvent.promise; - throw new Error("completed tool event delivery failed"); - } - return super.pushEvents(input); - } - } - - const socket = new FailingCompletedEventSocket([command]); - let providerCalls = 0; - const runtime = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => socket.isDrained(), - mcpExecute: async () => { - providerCalls += 1; - return result; - }, - runtimeState: new DriverRuntimeStateMachine("ready"), - }); - const runTask = runtime.dispatcher.run(socket, runtime.logger); - - await durable.promise; - await completedEventEntered.promise; - expect(socket.updates.filter(({ status }) => status !== "accepted")).toEqual([]); - releaseCompletedEvent.resolve(); - await runTask; - await waitForUpdate( - socket, - (update) => update.commandId === command.commandId && update.status === "completed", - ); - - expect(providerCalls).toBe(settlement === "executed" ? 1 : 0); - expect(socket.unknownCount).toBe(0); - expect(socket.updates.filter(({ status }) => status !== "accepted")).toEqual([ - { commandId: command.commandId, result, status: "completed" }, - ]); - }, - ); - test.each([ [ - "invalid arguments", - withMcpServers([ - { - authType: "bearer", - authorizationState: "active", - credentialId: MCP_PREFLIGHT_CREDENTIAL_ID, - credentialScope: "session", - credentialStatus: "active", - name: "Test MCP", - proxyGrantId: "test-grant", - proxyUrl: "https://mcp.invalid.test", - serverId: MCP_PREFLIGHT_SERVER_ID, - }, - ]), - "{", - MCP_PREFLIGHT_SERVER_ID, - "Invalid MCP tool arguments", - ], - ["missing server", withMcpServers([]), "{}", MCP_PREFLIGHT_SERVER_ID, "not configured"], - [ - "disabled server", - withMcpServers([ - { - authType: "bearer", - authorizationState: "disabled", - credentialScope: "session", - credentialStatus: "disabled", - name: "Test MCP", - serverId: MCP_PREFLIGHT_SERVER_ID, - }, - ]), - "{}", - MCP_PREFLIGHT_SERVER_ID, - "disabled for this session", - ], - ] as const)( - "rejects %s before claiming an external effect", - async (_name, payload, argumentsJson, serverId, message) => { - const command: McpExecuteCommand = { - argumentsJson, - commandId: `preflight-${_name.replaceAll(" ", "-")}`, - kind: "mcp.execute", - requestId: "preflight-request", - serverId, - toolCallId: "preflight-tool", - toolName: "lookup", - }; - - const socket = new EffectLedgerRecordingSocket([command]); - const runtime = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => socket.isDrained(), - mcpPrepare: (input, signal) => prepareRemoteHttpMcpCommand(payload, input, signal), - runtimeState: new DriverRuntimeStateMachine("ready"), - }); - - await runtime.dispatcher.run(socket, runtime.logger); - await waitForUpdate( - socket, - (update) => update.commandId === command.commandId && update.status === "failed", - ); - const failure = socket.updates.find( - (update) => update.commandId === command.commandId && update.status === "failed", - ); - - expect(failure?.error?.message).toContain(message); - expect(socket.claimCount).toBe(0); - expect(socket.unknownCount).toBe(0); - }, - ); - - test("does not claim an external effect when MCP connection setup fails", async () => { - let requestCount = 0; - const server = Bun.serve({ - hostname: "127.0.0.1", - port: 0, - fetch() { - requestCount += 1; - return new Response("unavailable", { status: 503 }); - }, - }); - const payload = withMcpServers([ - { - authType: "bearer", - authorizationState: "active", - credentialId: MCP_PREFLIGHT_CREDENTIAL_ID, - credentialScope: "session", - credentialStatus: "active", - name: "Test MCP", - proxyGrantId: "test-grant", - proxyUrl: `http://${server.hostname}:${server.port}`, - serverId: MCP_PREFLIGHT_SERVER_ID, - }, - ]); - const command: McpExecuteCommand = { - argumentsJson: "{}", - commandId: "preflight-connect-failure", - kind: "mcp.execute", - requestId: "preflight-connect-request", - serverId: MCP_PREFLIGHT_SERVER_ID, - toolCallId: "preflight-connect-tool", - toolName: "lookup", - }; - const socket = new EffectLedgerRecordingSocket([command]); - const runtime = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => socket.isDrained(), - mcpPrepare: (input, signal) => prepareRemoteHttpMcpCommand(payload, input, signal), - runtimeState: new DriverRuntimeStateMachine("ready"), - }); - - try { - await runtime.dispatcher.run(socket, runtime.logger); - await waitForUpdate( - socket, - (update) => update.commandId === command.commandId && update.status === "failed", - ); - const failure = socket.updates.find( - (update) => update.commandId === command.commandId && update.status === "failed", - ); - - expect(requestCount).toBeGreaterThan(0); - expect(failure?.error?.message).toContain("HTTP 503"); - expect(socket.claimCount).toBe(0); - expect(socket.unknownCount).toBe(0); - } finally { - await server.stop(true); - } - }); - - test.each([ - ["before prepare", 0, 0], - ["during prepare", 0, 0], - ["during claim", 1, 1], - ] as const)("linearizes MCP cancellation %s", async (stage, expectedClaims, expectedUnknowns) => { - const command: McpExecuteCommand = { - argumentsJson: "{}", - commandId: `cancel-${stage.replace(" ", "-")}`, - kind: "mcp.execute", - requestId: "cancel-preparation-request", - serverId: MCP_PREFLIGHT_SERVER_ID, - toolCallId: "cancel-preparation-tool", - toolName: "lookup", - }; - const boundaryEntered = Promise.withResolvers(); - const releaseBoundary = Promise.withResolvers(); - - class CancellationSocket extends EffectLedgerRecordingSocket { - #blocked = false; - - override async claimExternalToolEffect( - input: Parameters[0], - signal: AbortSignal, - ): ReturnType { - if (stage !== "during claim") { - return super.claimExternalToolEffect(input, signal); - } - this.claimCount += 1; - boundaryEntered.resolve(); - return new Promise((_, reject) => { - signal.addEventListener("abort", () => reject(signal.reason), { once: true }); - }); - } - - override async pushEvents( - input: Parameters[0], - ): ReturnType { - if (stage === "before prepare" && !this.#blocked) { - this.#blocked = true; - boundaryEntered.resolve(); - await releaseBoundary.promise; - } - return super.pushEvents(input); - } - } - - const socket = new CancellationSocket([command]); - const shutdown = new AbortController(); - let executions = 0; - let preparations = 0; - const runtime = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => shutdown.signal.aborted, - mcpPrepare: async () => { - preparations += 1; - if (stage === "during prepare") { - boundaryEntered.resolve(); - await releaseBoundary.promise; - } - return { - async execute() { - executions += 1; - throw new Error("cancelled preparation must not execute"); - }, - async [Symbol.asyncDispose]() {}, - }; - }, - runtimeState: new DriverRuntimeStateMachine("ready"), - shutdownSignal: shutdown.signal, - }); - const runTask = runtime.dispatcher.run(socket, runtime.logger); - - await boundaryEntered.promise; - shutdown.abort(new Error("test cancellation")); - releaseBoundary.resolve(); - await runTask; - - expect(preparations).toBe(stage === "before prepare" ? 0 : 1); - expect(executions).toBe(0); - expect(socket.claimCount).toBe(expectedClaims); - expect(socket.unknownCount).toBe(expectedUnknowns); - expect(socket.updates).toContainEqual({ - commandId: command.commandId, - status: "cancelled", - }); - }); - - test("fences an MCP claim when its acknowledgement is lost", async () => { - const command: McpExecuteCommand = { - argumentsJson: "{}", - commandId: "lost-claim-ack", - kind: "mcp.execute", - requestId: "lost-claim-request", - serverId: MCP_PREFLIGHT_SERVER_ID, - toolCallId: "lost-claim-tool", - toolName: "lookup", - }; - - class LostClaimSocket extends FakeDriverRuntimeIo { - fenceSignalAborted: boolean | null = null; - unknownCount = 0; - - override async claimExternalToolEffect(): ReturnType< - FakeDriverRuntimeIo["claimExternalToolEffect"] - > { - throw new Error("claim acknowledgement lost"); - } - - override async markExternalToolEffectUnknown( - _input: Parameters[0], - signal: AbortSignal, - ): Promise { - this.unknownCount += 1; - this.fenceSignalAborted = signal.aborted; - } - } - - const socket = new LostClaimSocket([command]); - const runtime = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => socket.isDrained(), - runtimeState: new DriverRuntimeStateMachine("ready"), - }); - - await runtime.dispatcher.run(socket, runtime.logger); - await waitForUpdate( - socket, - (update) => update.commandId === command.commandId && update.status === "failed", - ); - - expect(socket.unknownCount).toBe(1); - expect(socket.fenceSignalAborted).toBe(false); - }); - - test("fences an in-flight MCP effect after the shutdown signal aborts", async () => { - const mcpCommand: RuntimeCommand = { - argumentsJson: '{"title":"cancel safely"}', - commandId: "cancelled-effect-command", - kind: "mcp.execute", - requestId: "cancelled-effect-request", - serverId: "mcp-linear", - toolCallId: "tool-cancelled-effect", - toolName: "createIssue", - }; - const mcpStarted = Promise.withResolvers(); - - class CancellationSocket extends FakeDriverRuntimeIo { - readonly #nextCommand = Promise.withResolvers(); - #readFirstCommand = true; - #drained = false; - fenceSignalAborted: boolean | null = null; - fencedCommandId: string | null = null; - - constructor() { - super([]); - } - - override isDrained(): boolean { - return this.#drained; - } - - override async markExternalToolEffectUnknown( - input: { commandId: string }, - signal: AbortSignal, - ): Promise { - this.fencedCommandId = input.commandId; - this.fenceSignalAborted = signal.aborted; - } - - override nextCommand(_signal: AbortSignal): Promise { - if (this.#readFirstCommand) { - this.#readFirstCommand = false; - return Promise.resolve(mcpCommand); - } - - return this.#nextCommand.promise; - } - - stop(): void { - this.#drained = true; - this.#nextCommand.resolve(null); - } - } - - const socket = new CancellationSocket(); - const shutdown = new AbortController(); - const runtimeState = new DriverRuntimeStateMachine("ready"); - const runtime = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => socket.isDrained(), - mcpExecute: async (_command, signal) => { - mcpStarted.resolve(); - return new Promise((_resolve, reject) => { - signal.addEventListener("abort", () => reject(signal.reason), { once: true }); - }); - }, - runtimeState, - shutdownSignal: shutdown.signal, - }); - const runTask = runtime.dispatcher.run(socket, runtime.logger); - - await mcpStarted.promise; - runtimeState.enter("stopping"); - socket.stop(); - shutdown.abort(new Error("test shutdown")); - await runTask; - - expect(socket.fenceSignalAborted).toBe(false); - expect(socket.fencedCommandId).toBe(mcpCommand.commandId); - expect(socket.updates).toMatchObject([ - { commandId: mcpCommand.commandId, status: "accepted" }, - { commandId: mcpCommand.commandId, status: "cancelled" }, - ]); - }); - - test("persists a known MCP result after shutdown with a fresh retry budget", async () => { - const command: McpExecuteCommand = { - argumentsJson: "{}", - commandId: "known-result-during-shutdown", - kind: "mcp.execute", - requestId: "known-result-request", - serverId: MCP_PREFLIGHT_SERVER_ID, - toolCallId: "known-result-tool", - toolName: "lookup", - }; - const providerStarted = Promise.withResolvers(); - const releaseProvider = Promise.withResolvers(); - - class CompletionSocket extends FakeDriverRuntimeIo { - readonly #nextCommand = Promise.withResolvers(); - #readFirstCommand = true; - #drained = false; - completionAttempts = 0; - readonly completionSignals: boolean[] = []; - unknownCount = 0; - - constructor() { - super([]); - } - - override isDrained(): boolean { - return this.#drained; - } - - override nextCommand(_signal: AbortSignal): Promise { - if (this.#readFirstCommand) { - this.#readFirstCommand = false; - return Promise.resolve(command); - } - - return this.#nextCommand.promise; - } - - override async completeExternalToolEffect( - _input: Parameters[0], - signal: AbortSignal, - ): Promise { - this.completionAttempts += 1; - this.completionSignals.push(signal.aborted); - if (this.completionAttempts === 1) { - throw new Error("completion acknowledgement lost"); - } - } - - override async markExternalToolEffectUnknown(): Promise { - this.unknownCount += 1; - } - - stop(): void { - this.#drained = true; - this.#nextCommand.resolve(null); - } - } - - const socket = new CompletionSocket(); - const shutdown = new AbortController(); - const runtimeState = new DriverRuntimeStateMachine("ready"); - const runtime = createDispatcher({ - backend: createBackend(), - isShuttingDown: () => socket.isDrained(), - mcpExecute: async (input) => { - providerStarted.resolve(); - await releaseProvider.promise; - return { - outputText: "known result", - requestId: input.requestId, - serverId: input.serverId, - toolName: input.toolName, - }; - }, - runtimeState, - shutdownSignal: shutdown.signal, - }); - const runTask = runtime.dispatcher.run(socket, runtime.logger); - - await providerStarted.promise; - runtimeState.enter("stopping"); - socket.stop(); - shutdown.abort(new Error("test shutdown")); - releaseProvider.resolve(); - await runTask; - - expect(socket.completionAttempts).toBe(2); - expect(socket.completionSignals).toEqual([false, false]); - expect(socket.unknownCount).toBe(0); - expect(socket.updates).toMatchObject([ - { commandId: command.commandId, status: "accepted" }, + "changed content", { - commandId: command.commandId, - result: { - outputText: "known result", - requestId: command.requestId, - serverId: command.serverId, - toolName: command.toolName, - }, - status: "completed", + commandId: "reused-command", + kind: "turn.cancel", + reason: "second reason", + runId: DRIVER_TEST_IDS.runId, }, - ]); - }); - - test.each([ - [ - "changed content", - { commandId: "reused-command", kind: "turn.cancel", reason: "second reason" }, ], [ "changed kind", @@ -1216,16 +416,25 @@ describe("driver runtime boundary", () => { decision: "reject_once", kind: "permission.resolve", requestId: "permission-1", + runId: DRIVER_TEST_IDS.runId, }, ], ] satisfies readonly (readonly [string, RuntimeCommand])[])( "rejects a completed command ID replay with %s", async (_case, replay) => { const backend = createBackend(); - const socket = new FakeDriverRuntimeIo([ - { commandId: "reused-command", kind: "turn.cancel", reason: "first reason" }, - replay, - ]); + const socket = new FakeDriverRuntimeIo( + [ + { + commandId: "reused-command", + kind: "turn.cancel", + reason: "first reason", + runId: DRIVER_TEST_IDS.runId, + }, + replay, + ], + DRIVER_TEST_IDS.runId, + ); const runtimeState = new DriverRuntimeStateMachine("ready"); const { dispatcher, logger } = createDispatcher({ backend, runtimeState }); @@ -1251,7 +460,7 @@ describe("driver runtime boundary", () => { const firstInputCanFinish = Promise.withResolvers(); const backend = createBackend(); let handledInputCount = 0; - backend.handleInput = async (context) => { + backend.handleInput = async (context, _input, runId) => { handledInputCount += 1; backend.handledInputs.push(context.payload.execution.session); @@ -1259,6 +468,16 @@ describe("driver runtime boundary", () => { firstInputStarted.resolve(); await firstInputCanFinish.promise; } + await context.ports.eventSink.pushEvents({ + events: [ + { + kind: "run.completed", + payload: { status: "completed" }, + runId, + sourceEventId: `queued-input.completed:${runId}`, + }, + ], + }); }; const runtimeState = new DriverRuntimeStateMachine("ready"); const socket = new FakeDriverRuntimeIo([ diff --git a/tests/driver-runtime-boundary-timing.test.ts b/tests/driver-runtime-boundary-timing.test.ts index 9598e13..48ca32e 100644 --- a/tests/driver-runtime-boundary-timing.test.ts +++ b/tests/driver-runtime-boundary-timing.test.ts @@ -3,13 +3,14 @@ import { describe, expect, test } from "bun:test"; import { ACTIVE_TURN_CANCEL_GRACE_MS } from "../src/core/driver-command-dispatcher"; import { DRIVER_EVENT_DELIVERY_TIMEOUT_MS } from "../src/core/driver-runtime-io"; import { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; -import type { RuntimeCommand } from "../src/runtime-command"; +import type { McpExternalToolExecutionResult, RuntimeCommand } from "../src/runtime-command"; import { settlePromiseWithTimeout } from "../src/utils/async"; import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; import { FakeDriverRuntimeIo, createBackend, createDispatcher, + settleBackendInput, waitForUpdate, } from "./driver-runtime-boundary-fixtures"; @@ -30,6 +31,7 @@ describe("driver runtime boundary", () => { commandId: "cancel-with-lossless-terminal", kind: "turn.cancel", reason: "test.cancel", + runId: DRIVER_TEST_IDS.runId, }, ]); const nativeSetTimeout = globalThis.setTimeout; @@ -45,15 +47,7 @@ describe("driver runtime boundary", () => { await new Promise((resolve) => { signal!.addEventListener("abort", () => nativeSetTimeout(resolve, 30), { once: true }); }); - await context.ports.eventSink.pushEvents({ - events: [ - { - kind: "run.cancelled", - payload: { reason: "test.cancel" }, - runId, - }, - ], - }); + await settleBackendInput(context, runId, signal); }; const { dispatcher, logger } = createDispatcher({ backend, @@ -102,9 +96,10 @@ describe("driver runtime boundary", () => { const entered = Promise.withResolvers(); const release = Promise.withResolvers(); const backend = createBackend(); - backend.handleInput = async () => { + backend.handleInput = async (context, _input, runId, signal) => { entered.resolve(); await release.promise; + await settleBackendInput(context, runId, signal); }; const shutdown = new AbortController(); const runtimeState = new DriverRuntimeStateMachine("ready"); @@ -160,7 +155,16 @@ describe("driver runtime boundary", () => { const runtimeState = new DriverRuntimeStateMachine("ready"); let boundarySignal: AbortSignal | undefined; const socket = new FakeDriverRuntimeIo( - boundary === "poll" ? [] : [{ commandId: "accepted-never-settles", kind: "turn.cancel" }], + boundary === "poll" + ? [] + : [ + { + commandId: "accepted-never-settles", + kind: "turn.cancel", + runId: DRIVER_TEST_IDS.runId, + }, + ], + boundary === "poll" ? undefined : DRIVER_TEST_IDS.runId, ); if (boundary === "poll") { socket.nextCommand = async (signal) => { @@ -211,13 +215,17 @@ describe("driver runtime boundary", () => { const entered = Promise.withResolvers(); const late = Promise.withResolvers(); const runtimeState = new DriverRuntimeStateMachine("ready"); - const socket = new FakeDriverRuntimeIo([ - { - commandId: "accepted-timeout", - kind: "turn.cancel", - reason: "must not run", - }, - ]); + const socket = new FakeDriverRuntimeIo( + [ + { + commandId: "accepted-timeout", + kind: "turn.cancel", + reason: "must not run", + runId: DRIVER_TEST_IDS.runId, + }, + ], + DRIVER_TEST_IDS.runId, + ); let acceptedSignal: AbortSignal | undefined; socket.commandUpdate = async (update, signal) => { if (update.status === "accepted") { @@ -265,17 +273,21 @@ describe("driver runtime boundary", () => { test("keeps MCP commands explicit at the API boundary", async () => { const backend = createBackend(); const runtimeState = new DriverRuntimeStateMachine("ready"); - const socket = new FakeDriverRuntimeIo([ - { - argumentsJson: '{"issue":"A-1"}', - commandId: "mcp-1", - kind: "mcp.execute", - requestId: "mcp-request-1", - serverId: "mcp-linear", - toolCallId: "tool-mcp-1", - toolName: "createIssue", - }, - ]); + const socket = new FakeDriverRuntimeIo( + [ + { + argumentsJson: '{"issue":"A-1"}', + commandId: "mcp-1", + kind: "mcp.execute", + requestId: "mcp-request-1", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: "tool-mcp-1", + toolName: "createIssue", + }, + ], + DRIVER_TEST_IDS.runId, + ); const { commandReads, dispatcher, logger } = createDispatcher({ backend, isShuttingDown: () => @@ -334,20 +346,24 @@ describe("driver runtime boundary", () => { ]); }); - test("reports remote MCP execute failures as diagnostics", async () => { + test("fences remote MCP execute failures as unknown diagnostics", async () => { const backend = createBackend(); const runtimeState = new DriverRuntimeStateMachine("ready"); - const socket = new FakeDriverRuntimeIo([ - { - argumentsJson: '{"issue":"A-1"}', - commandId: "mcp-1", - kind: "mcp.execute", - requestId: "mcp-request-1", - serverId: "mcp-linear", - toolCallId: "tool-mcp-1", - toolName: "createIssue", - }, - ]); + const socket = new FakeDriverRuntimeIo( + [ + { + argumentsJson: '{"issue":"A-1"}', + commandId: "mcp-1", + kind: "mcp.execute", + requestId: "mcp-request-1", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: "tool-mcp-1", + toolName: "createIssue", + }, + ], + DRIVER_TEST_IDS.runId, + ); const { dispatcher, logger } = createDispatcher({ backend, isShuttingDown: () => @@ -368,8 +384,16 @@ describe("driver runtime boundary", () => { { commandId: "mcp-1", error: { - code: "driver.command_failed.mcp.execute", - message: "MCP upstream failed", + code: "driver.external_tool_effect_unknown", + details: { + commandId: "mcp-1", + effectId: "test-effect-mcp-1", + requestId: "mcp-request-1", + serverId: "mcp-linear", + toolName: "createIssue", + }, + message: expect.stringContaining("unknown outcome"), + retryable: false, }, status: "failed", }, @@ -391,7 +415,7 @@ describe("driver runtime boundary", () => { { kind: "tool.call.updated", payload: { - rawOutput: "MCP upstream failed", + rawOutput: expect.stringContaining("unknown outcome"), status: "failed", toolCallId: "tool-mcp-1", }, @@ -410,7 +434,7 @@ describe("driver runtime boundary", () => { serverId: "mcp-linear", toolName: "createIssue", }, - message: "MCP upstream failed", + message: expect.stringContaining("unknown outcome"), severity: "error", source: "core", }, @@ -420,55 +444,54 @@ describe("driver runtime boundary", () => { ]); }); - test("lets session stop preempt a stuck MCP command", async () => { + test("lets session stop preempt polling and join a committed MCP effect", async () => { const backend = createBackend(); - const aborted = Promise.withResolvers(); - const releaseCleanup = Promise.withResolvers(); + const entered = Promise.withResolvers(); + const execution = Promise.withResolvers(); const runtimeState = new DriverRuntimeStateMachine("ready"); - const socket = new FakeDriverRuntimeIo([ - { - argumentsJson: "{}", - commandId: "mcp-stuck", - kind: "mcp.execute", - requestId: "mcp-request-stuck", - serverId: "mcp-linear", - toolCallId: "tool-mcp-stuck", - toolName: "waitForever", - }, - { - commandId: "stop-1", - kind: "session.stop", - reason: "test.stop", - }, - ]); + const socket = new FakeDriverRuntimeIo( + [ + { + argumentsJson: "{}", + commandId: "mcp-stuck", + kind: "mcp.execute", + requestId: "mcp-request-stuck", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: "tool-mcp-stuck", + toolName: "waitForever", + }, + { + commandId: "stop-1", + kind: "session.stop", + reason: "test.stop", + }, + ], + DRIVER_TEST_IDS.runId, + ); const { dispatcher, logger, shutdownCalls } = createDispatcher({ backend, isShuttingDown: () => socket.isDrained(), - mcpExecute: async (_command, signal) => { - return await new Promise((_resolve, reject) => { - signal.addEventListener( - "abort", - async () => { - aborted.resolve(); - await releaseCleanup.promise; - reject(signal.reason); - }, - { once: true }, - ); - }); + mcpExecute: async () => { + entered.resolve(); + return execution.promise; }, runtimeState, }); const run = dispatcher.run(socket, logger); - await aborted.promise; + await entered.promise; + await waitForUpdate( + socket, + (update) => update.commandId === "stop-1" && update.status === "accepted", + ); expect(await Promise.race([run.then(() => true), Bun.sleep(10).then(() => false)])).toBe(false); - releaseCleanup.resolve(); + execution.reject(new Error("provider response lost")); await run; await waitForUpdate( socket, - (update) => update.commandId === "mcp-stuck" && update.status === "cancelled", + (update) => update.commandId === "mcp-stuck" && update.status === "failed", ); expect(runtimeState.status()).toBe("stopped"); @@ -476,7 +499,23 @@ describe("driver runtime boundary", () => { expect(socket.updates).toEqual( expect.arrayContaining([ { commandId: "mcp-stuck", status: "accepted" }, - { commandId: "mcp-stuck", status: "cancelled" }, + { + commandId: "mcp-stuck", + error: { + code: "driver.external_tool_effect_unknown", + details: { + commandId: "mcp-stuck", + effectId: "test-effect-mcp-stuck", + requestId: "mcp-request-stuck", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolName: "waitForever", + }, + message: expect.stringContaining("unknown outcome"), + retryable: false, + }, + status: "failed", + }, { commandId: "stop-1", status: "accepted" }, { commandId: "stop-1", status: "completed" }, ]), @@ -488,13 +527,17 @@ describe("driver runtime boundary", () => { const updateEntered = Promise.withResolvers(); const updateResult = Promise.withResolvers(); const runtimeState = new DriverRuntimeStateMachine("ready"); - const socket = new FakeDriverRuntimeIo([ - { - commandId: "cancel-during-shutdown", - kind: "turn.cancel", - reason: "test.cancel", - }, - ]); + const socket = new FakeDriverRuntimeIo( + [ + { + commandId: "cancel-during-shutdown", + kind: "turn.cancel", + reason: "test.cancel", + runId: DRIVER_TEST_IDS.runId, + }, + ], + DRIVER_TEST_IDS.runId, + ); socket.commandUpdate = async () => { updateEntered.resolve(); await updateResult.promise; @@ -527,6 +570,7 @@ describe("driver runtime boundary", () => { commandId: `mcp-${index}`, kind: "mcp.execute" as const, requestId: `mcp-request-${index}`, + runId: DRIVER_TEST_IDS.runId, serverId: "mcp-linear", toolCallId: `tool-mcp-${index}`, toolName: "waitForever", @@ -537,21 +581,32 @@ describe("driver runtime boundary", () => { reason: "test.stop", }, ]; - const socket = new FakeDriverRuntimeIo(commands); + const socket = new FakeDriverRuntimeIo(commands, DRIVER_TEST_IDS.runId); let executeCalls = 0; + const releaseExecutions = Promise.withResolvers(); const { dispatcher, logger } = createDispatcher({ backend, isShuttingDown: () => socket.isDrained(), - mcpExecute: async (_command, signal) => { + mcpExecute: async (command) => { executeCalls += 1; - return await new Promise((_resolve, reject) => { - signal.addEventListener("abort", () => reject(signal.reason), { once: true }); - }); + await releaseExecutions.promise; + return { + outputText: "finished after stop", + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }; }, runtimeState, }); - await dispatcher.run(socket, logger); + const run = dispatcher.run(socket, logger); + await waitForUpdate( + socket, + (update) => update.commandId === "stop-after-mcp-limit" && update.status === "accepted", + ); + releaseExecutions.resolve(); + await run; await waitForUpdate( socket, (update) => update.commandId === "mcp-32" && update.status === "failed", diff --git a/tests/driver-terminal-state.test.ts b/tests/driver-terminal-state.test.ts new file mode 100644 index 0000000..ec3548c --- /dev/null +++ b/tests/driver-terminal-state.test.ts @@ -0,0 +1,222 @@ +import { describe, expect, test } from "bun:test"; + +import { + DriverTerminalStateMachine, + type DriverRunTerminalIdentity, +} from "../src/core/driver-terminal-state"; +import { DriverTurnCancelledError } from "../src/core/driver-turn-cancelled-error"; +import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; + +function terminal( + status: "cancelled" | "completed" | "failed", + sourceEventId = `terminal-${status}`, +): DriverRunTerminalIdentity { + return { + event: + status === "failed" + ? { + kind: "run.failed", + payload: { error: { code: "test.failed", message: "failed", retryable: false } }, + sourceEventId, + } + : status === "completed" + ? { + kind: "run.completed", + payload: { stopReason: "end_turn" }, + sourceEventId, + } + : { + kind: "run.cancelled", + payload: { stopReason: "cancelled" }, + sourceEventId, + }, + runId: DRIVER_TEST_IDS.runId, + sourceEventId, + status, + }; +} + +function receipt(identity: DriverRunTerminalIdentity) { + return { eventId: identity.sourceEventId, seq: 1, type: identity.event.kind }; +} + +describe("DriverTerminalStateMachine", () => { + test("linearizes cancellation before a completed terminal", () => { + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.runId); + + expect(state.claimCancellation(ticket, "cancel first")).toBe("claimed"); + expect(state.claimCancellation(ticket, "again")).toBe("already_claimed"); + expect(ticket.signal.reason).toBeInstanceOf(DriverTurnCancelledError); + expect(state.selectRunTerminal(ticket, terminal("completed"))).toBe("cancelled"); + }); + + test("linearizes terminal selection before cancellation and requires its exact ACK", () => { + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.runId); + const selected = terminal("completed"); + + expect(state.selectRunTerminal(ticket, selected)).toBe("selected"); + expect(state.claimCancellation(ticket, "too late")).toBe("terminal_selected"); + expect(state.selectRunTerminal(ticket, structuredClone(selected))).toBe("pending"); + expect(() => + state.selectRunTerminal(ticket, { + ...selected, + event: { ...selected.event, payload: { stopReason: "other" } }, + }), + ).toThrow("conflicts"); + expect(() => state.ackRunTerminal(ticket, { ...receipt(selected), eventId: "wrong" })).toThrow( + "does not match", + ); + + state.ackRunTerminal(ticket, receipt(selected)); + expect(state.selectRunTerminal(ticket, selected)).toBe("acked"); + expect(state.snapshotRun()?.terminal?.phase).toBe("acked"); + }); + + test("fails closed when cancellation has no acknowledged run terminal", () => { + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.runId); + state.claimCancellation(ticket, "cancelled"); + + const rejection = new Error("provider cleanup rejected"); + expect(state.settleInput(ticket, { error: rejection, status: "rejected" })).toEqual({ + failure: rejection, + status: "failed", + }); + expect(state.settleInput(ticket, { status: "resolved" })).toMatchObject({ + failure: { message: "Driver input settled without a run terminal." }, + status: "failed", + }); + }); + + test("keeps an acknowledged failed terminal authoritative over cancellation", () => { + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.runId); + const failed = terminal("failed"); + state.claimCancellation(ticket, "cancelled"); + state.selectRunTerminal(ticket, failed); + state.ackRunTerminal(ticket, receipt(failed)); + + expect( + state.settleInput(ticket, { + error: new DriverTurnCancelledError("backend cancelled"), + status: "cancelled", + }), + ).toMatchObject({ + failure: { message: "Driver cancellation settled with a failed run terminal." }, + status: "failed", + }); + }); + + test.each([ + ["cancelled + resolved", "cancelled", { status: "resolved" }, "failed"], + [ + "cancelled + cancelled", + "cancelled", + { error: new DriverTurnCancelledError("backend cancelled"), status: "cancelled" }, + "cancelled", + ], + [ + "completed + cancelled", + "completed", + { error: new DriverTurnCancelledError("backend cancelled"), status: "cancelled" }, + "failed", + ], + ] as const)( + "settles acknowledged %s without a cancellation claim", + (_case, status, outcome, expected) => { + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.runId); + const selected = terminal(status); + state.selectRunTerminal(ticket, selected); + state.ackRunTerminal(ticket, receipt(selected)); + + expect(state.settleInput(ticket, outcome).status).toBe(expected); + }, + ); + + test("requires a selected terminal to be acknowledged before normal release", () => { + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.runId); + const selected = terminal("completed"); + state.selectRunTerminal(ticket, selected); + + expect(state.settleInput(ticket, { status: "resolved" }).status).toBe("failed"); + expect(() => state.releaseRun(ticket, "command_acked")).toThrow("acknowledged terminal"); + state.ackRunTerminal(ticket, receipt(selected)); + expect(state.settleInput(ticket, { status: "resolved" })).toEqual({ status: "resolved" }); + state.releaseRun(ticket, "command_acked"); + expect(() => state.claimCancellation(ticket, "stale")).toThrow("stale"); + }); + + test("cannot acknowledge a backend result without a durable run terminal", () => { + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.runId); + + expect(state.settleInput(ticket, { status: "resolved" })).toMatchObject({ + failure: { message: "Driver input settled without a run terminal." }, + status: "failed", + }); + expect(() => state.releaseRun(ticket, "command_acked")).toThrow( + "without an acknowledged terminal", + ); + expect(() => state.releaseRun(ticket, "driver_failing")).not.toThrow(); + }); + + test("keeps one exact instance terminal from selection through acknowledgement", () => { + const state = new DriverTerminalStateMachine(); + const failure = { + error: { code: "driver.failed", details: {}, message: "failed", retryable: false }, + runId: DRIVER_TEST_IDS.runId, + status: "failed", + } as const; + + expect(state.selectInstanceTerminal(failure)).toBe("selected"); + expect(state.selectInstanceTerminal(structuredClone(failure))).toBe("pending"); + expect(() => + state.selectInstanceTerminal({ runId: DRIVER_TEST_IDS.runId, status: "completed" }), + ).toThrow("conflicts"); + state.ackInstanceTerminal(failure); + expect(state.selectInstanceTerminal(failure)).toBe("acked"); + expect(() => state.beginRun(DRIVER_TEST_IDS.secondRunId)).toThrow("instance terminal"); + }); + + test("freezes the exact run owner before a later run can replace it", () => { + const state = new DriverTerminalStateMachine(); + const first = state.beginRun(DRIVER_TEST_IDS.runId); + const selected = terminal("completed"); + state.selectRunTerminal(first, selected); + state.ackRunTerminal(first, receipt(selected)); + state.releaseRun(first, "command_acked"); + + expect(state.terminalRunId()).toBe(DRIVER_TEST_IDS.runId); + expect( + state.selectInstanceTerminal({ runId: state.terminalRunId()!, status: "completed" }), + ).toBe("selected"); + expect(state.terminalRunId(DRIVER_TEST_IDS.secondRunId)).toBe(DRIVER_TEST_IDS.runId); + }); + + test("retains the shutdown failure owner after its run is released", () => { + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.runId); + const error = { code: "driver.failed", details: {}, message: "failed", retryable: false }; + + state.recordFailure(error); + state.releaseRun(ticket, "driver_failing"); + state.markCleanupCompleted(); + expect(state.shutdownSnapshot()).toEqual({ + cleanup: "completed", + failure: { error, runId: DRIVER_TEST_IDS.runId }, + }); + }); + + test("retains the last owned run without a provider terminal", () => { + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.secondRunId); + + state.releaseRun(ticket, "driver_failing"); + + expect(state.terminalRunId(DRIVER_TEST_IDS.runId)).toBe(DRIVER_TEST_IDS.secondRunId); + }); +}); diff --git a/tests/external-tool-effect-settlement.test.ts b/tests/external-tool-effect-settlement.test.ts new file mode 100644 index 0000000..003beb2 --- /dev/null +++ b/tests/external-tool-effect-settlement.test.ts @@ -0,0 +1,158 @@ +import { describe, expect, test } from "bun:test"; +import { + createMcpUnknownEffectRunError, + createMcpUnsettledEffectRunError, +} from "@mosoo/agent-driver"; + +import { + createDurableMcpSucceededSettlement, + requireDurableMcpResultIdentity, +} from "../src/core/external-tool-effect-settlement"; +import { + measureRuntimeCommandJson, + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + type McpExternalToolEffectSettlement, + type McpExternalToolExecutionResult, +} from "../src/runtime-command"; +import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; + +const RESULT = { + outputText: "", + requestId: "request-boundary", + serverId: "server-boundary", + toolName: "createIssue", +} as const; + +function executionAtSettlementSize(byteLength: number, unit = "x"): McpExternalToolExecutionResult { + const create = (outputText: string) => succeeded({ ...RESULT, outputText }); + const baseBytes = measureRuntimeCommandJson(create("")); + const unitBytes = measureRuntimeCommandJson(create(unit)) - baseBytes; + const outputBytes = byteLength - baseBytes; + + if (outputBytes < 0) { + throw new Error("Requested settlement size is smaller than its fixed fields."); + } + + return { + ...RESULT, + outputText: + unit.repeat(Math.floor(outputBytes / unitBytes)) + "x".repeat(outputBytes % unitBytes), + }; +} + +function succeeded(execution: McpExternalToolExecutionResult): McpExternalToolEffectSettlement { + const { providerReceiptJson, ...result } = execution; + return { + kind: "succeeded", + ...(providerReceiptJson === undefined ? {} : { providerReceiptJson }), + result, + }; +} + +describe("durable MCP succeeded settlement", () => { + test("defines the exact cross-process repair failures", () => { + const command = { + commandId: "command-repair", + requestId: RESULT.requestId, + runId: DRIVER_TEST_IDS.runId, + serverId: RESULT.serverId, + toolName: RESULT.toolName, + }; + + expect(createMcpUnknownEffectRunError(command, "effect-repair")).toEqual({ + code: "driver.external_tool_effect_unknown", + details: { ...command, effectId: "effect-repair" }, + message: + "External effect effect-repair for MCP tool createIssue has an unknown outcome and will not be replayed.", + retryable: false, + }); + expect(createMcpUnsettledEffectRunError(command, "effect-repair")).toEqual({ + code: "driver.command_failed.mcp.execute", + details: { commandId: command.commandId, commandKind: "mcp.execute" }, + message: + "External effect effect-repair for MCP tool createIssue requires server-side repair.", + retryable: false, + }); + }); + + test.each(["x", "界", "\0"])( + "preserves the exact %p byte limit and omits output at limit plus one", + (unit) => { + const exactExecution = executionAtSettlementSize( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + unit, + ); + const identity = requireDurableMcpResultIdentity(RESULT); + const exact = createDurableMcpSucceededSettlement(exactExecution, identity); + expect(measureRuntimeCommandJson(exact)).toBe( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + ); + expect(exact.result).toEqual(exactExecution); + + const oversized = createDurableMcpSucceededSettlement( + executionAtSettlementSize(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES + 1, unit), + identity, + ); + expect(oversized).toEqual({ + kind: "succeeded", + result: { + ...RESULT, + isError: true, + outputText: + "MCP tool output was omitted because its durable settlement exceeded the 1044480-byte limit.", + }, + }); + expect(measureRuntimeCommandJson(oversized)).toBeLessThanOrEqual( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + ); + }, + ); + + test.each([ + ["escaped", "\0".repeat(Math.ceil(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES / 6))], + ["Unicode", "界".repeat(Math.ceil(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES / 3))], + ])("measures %s output after JSON escaping and UTF-8 encoding", (_label, outputText) => { + const execution = { ...RESULT, outputText }; + expect(outputText.length).toBeLessThan(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES); + expect(measureRuntimeCommandJson(succeeded(execution))).toBeGreaterThan( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + ); + + const normalized = createDurableMcpSucceededSettlement( + execution, + requireDurableMcpResultIdentity(RESULT), + ); + expect(normalized.kind).toBe("succeeded"); + expect(normalized.result.isError).toBeTrue(); + expect(normalized.result.outputText).toContain("output was omitted"); + }); + + test("drops an oversized diagnostic receipt before changing provider output", () => { + const execution = { + ...RESULT, + outputText: "provider result", + providerReceiptJson: JSON.stringify({ + diagnostic: "r".repeat(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES), + }), + }; + expect(measureRuntimeCommandJson(succeeded(execution))).toBeGreaterThan( + RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES, + ); + + expect( + createDurableMcpSucceededSettlement(execution, requireDurableMcpResultIdentity(RESULT)), + ).toEqual({ + kind: "succeeded", + result: { ...RESULT, outputText: "provider result" }, + }); + }); + + test("rejects an unpersistable result identity before execution", () => { + expect(() => + requireDurableMcpResultIdentity({ + ...RESULT, + requestId: "r".repeat(RUNTIME_COMMAND_TERMINAL_PAYLOAD_MAX_UTF8_BYTES), + }), + ).toThrow("MCP command identity exceeds the durable settlement byte limit."); + }); +}); diff --git a/tests/fixtures/driver/commands/input-start.json b/tests/fixtures/driver/commands/input-start.json index 5b5d68a..c754748 100644 --- a/tests/fixtures/driver/commands/input-start.json +++ b/tests/fixtures/driver/commands/input-start.json @@ -5,5 +5,5 @@ }, "kind": "input.start", "requestId": "request-1", - "runId": "run-1" + "runId": "01J00000000000000000000012" } diff --git a/tests/fixtures/driver/commands/mcp-execute.json b/tests/fixtures/driver/commands/mcp-execute.json index 8ce053d..12f0579 100644 --- a/tests/fixtures/driver/commands/mcp-execute.json +++ b/tests/fixtures/driver/commands/mcp-execute.json @@ -3,6 +3,7 @@ "commandId": "command-mcp-1", "kind": "mcp.execute", "requestId": "request-1", + "runId": "01J00000000000000000000012", "serverId": "server-1", "toolCallId": "tool-1", "toolName": "complete" diff --git a/tests/fixtures/driver/commands/permission-resolve.json b/tests/fixtures/driver/commands/permission-resolve.json index 73bd10e..49efaa7 100644 --- a/tests/fixtures/driver/commands/permission-resolve.json +++ b/tests/fixtures/driver/commands/permission-resolve.json @@ -2,5 +2,6 @@ "commandId": "command-permission-1", "decision": "allow_once", "kind": "permission.resolve", - "requestId": "permission-1" + "requestId": "permission-1", + "runId": "01J00000000000000000000012" } diff --git a/tests/fixtures/driver/commands/turn-cancel.json b/tests/fixtures/driver/commands/turn-cancel.json index e8deb47..993f17a 100644 --- a/tests/fixtures/driver/commands/turn-cancel.json +++ b/tests/fixtures/driver/commands/turn-cancel.json @@ -1,5 +1,6 @@ { "commandId": "command-cancel-1", "kind": "turn.cancel", - "reason": "user" + "reason": "user", + "runId": "01J00000000000000000000012" } diff --git a/tests/fixtures/driver/runtime-event-drafts/tool-call-deltas.json b/tests/fixtures/driver/runtime-event-drafts/tool-call-deltas.json new file mode 100644 index 0000000..b941ec1 --- /dev/null +++ b/tests/fixtures/driver/runtime-event-drafts/tool-call-deltas.json @@ -0,0 +1,13 @@ +{ + "delivery": "best_effort", + "kind": "tool.call.updated", + "payload": { + "future": { + "kept": true + }, + "rawInputDelta": "{\"path\":", + "rawOutputDelta": "chunk", + "status": "running", + "toolCallId": "tool-delta-1" + } +} diff --git a/tests/fixtures/driver/runtime-event-envelopes/agent-tasks-replaced.json b/tests/fixtures/driver/runtime-event-envelopes/agent-tasks-replaced.json index be4c92a..ea285b6 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/agent-tasks-replaced.json +++ b/tests/fixtures/driver/runtime-event-envelopes/agent-tasks-replaced.json @@ -17,7 +17,7 @@ }, "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", - "schemaVersion": "2026-05-26", + "schemaVersion": "2026-08-29", "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" diff --git a/tests/fixtures/driver/runtime-event-envelopes/diagnostic-reported.json b/tests/fixtures/driver/runtime-event-envelopes/diagnostic-reported.json index 2ebc25c..2a399c7 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/diagnostic-reported.json +++ b/tests/fixtures/driver/runtime-event-envelopes/diagnostic-reported.json @@ -13,7 +13,7 @@ }, "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", - "schemaVersion": "2026-05-26", + "schemaVersion": "2026-08-29", "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "owner_debug" diff --git a/tests/fixtures/driver/runtime-event-envelopes/message-delta.json b/tests/fixtures/driver/runtime-event-envelopes/message-delta.json index 83f3d93..08d3e51 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/message-delta.json +++ b/tests/fixtures/driver/runtime-event-envelopes/message-delta.json @@ -13,7 +13,7 @@ }, "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", - "schemaVersion": "2026-05-26", + "schemaVersion": "2026-08-29", "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" diff --git a/tests/fixtures/driver/runtime-event-envelopes/permission-requested.json b/tests/fixtures/driver/runtime-event-envelopes/permission-requested.json index d611748..a145c1e 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/permission-requested.json +++ b/tests/fixtures/driver/runtime-event-envelopes/permission-requested.json @@ -19,7 +19,7 @@ }, "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", - "schemaVersion": "2026-05-26", + "schemaVersion": "2026-08-29", "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" diff --git a/tests/fixtures/driver/runtime-event-envelopes/run-started.json b/tests/fixtures/driver/runtime-event-envelopes/run-started.json index 279108d..c43ee25 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/run-started.json +++ b/tests/fixtures/driver/runtime-event-envelopes/run-started.json @@ -14,7 +14,7 @@ }, "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", - "schemaVersion": "2026-05-26", + "schemaVersion": "2026-08-29", "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" diff --git a/tests/fixtures/driver/runtime-event-envelopes/tool-call-deltas.json b/tests/fixtures/driver/runtime-event-envelopes/tool-call-deltas.json new file mode 100644 index 0000000..16dbd92 --- /dev/null +++ b/tests/fixtures/driver/runtime-event-envelopes/tool-call-deltas.json @@ -0,0 +1,24 @@ +{ + "actor": "driver", + "delivery": "best_effort", + "driverInstanceId": "01J0000000000000000000000F", + "id": "01J0000000000000000000000G", + "kind": "tool.call.updated", + "occurredAt": "2026-05-26T00:00:00.000Z", + "origin": "driver", + "payload": { + "future": { + "kept": true + }, + "rawInputDelta": "{\"path\":", + "rawOutputDelta": "chunk", + "status": "running", + "toolCallId": "tool-delta-1" + }, + "runId": "01J00000000000000000000012", + "runtimeId": "runtime-1", + "schemaVersion": "2026-08-29", + "sessionId": "01J00000000000000000000008", + "traceId": "trace-1", + "visibility": "participant" +} diff --git a/tests/fixtures/driver/runtime-event-envelopes/tool-call-updated.json b/tests/fixtures/driver/runtime-event-envelopes/tool-call-updated.json index 66efdfd..d17ae04 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/tool-call-updated.json +++ b/tests/fixtures/driver/runtime-event-envelopes/tool-call-updated.json @@ -18,7 +18,7 @@ }, "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", - "schemaVersion": "2026-05-26", + "schemaVersion": "2026-08-29", "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" diff --git a/tests/fixtures/driver/runtime-event-envelopes/usage-updated.json b/tests/fixtures/driver/runtime-event-envelopes/usage-updated.json index 671ac74..09671fd 100644 --- a/tests/fixtures/driver/runtime-event-envelopes/usage-updated.json +++ b/tests/fixtures/driver/runtime-event-envelopes/usage-updated.json @@ -13,7 +13,7 @@ }, "runId": "01J00000000000000000000012", "runtimeId": "runtime-1", - "schemaVersion": "2026-05-26", + "schemaVersion": "2026-08-29", "sessionId": "01J00000000000000000000008", "traceId": "trace-1", "visibility": "participant" diff --git a/tests/fixtures/providers/acp/cases/max-turn-failure.json b/tests/fixtures/providers/acp/cases/max-turn-failure.json index 1a6edca..c3510d1 100644 --- a/tests/fixtures/providers/acp/cases/max-turn-failure.json +++ b/tests/fixtures/providers/acp/cases/max-turn-failure.json @@ -86,7 +86,8 @@ "payload": { "error": { "code": "acp.max_turn_requests", - "message": "ACP prompt stopped with max_turn_requests." + "message": "ACP prompt stopped with max_turn_requests.", + "retryable": false }, "recoverable": false, "stopReason": "max_turn_requests" diff --git a/tests/fixtures/providers/acp/cases/thought-and-unknown-update.json b/tests/fixtures/providers/acp/cases/thought-and-unknown-update.json index b34df05..c6f9640 100644 --- a/tests/fixtures/providers/acp/cases/thought-and-unknown-update.json +++ b/tests/fixtures/providers/acp/cases/thought-and-unknown-update.json @@ -74,7 +74,8 @@ "payload": { "error": { "code": "acp.empty_turn", - "message": "ACP prompt ended without assistant output or tool activity." + "message": "ACP prompt ended without assistant output or tool activity.", + "retryable": true }, "recoverable": true, "stopReason": "end_turn" diff --git a/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json b/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json index 1025313..cbd7a23 100644 --- a/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json +++ b/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json @@ -25,9 +25,7 @@ "event": { "content_block": { "id": "tool-1", - "input": { - "command": "pwd" - }, + "input": {}, "name": "Bash", "type": "tool_use" }, @@ -39,7 +37,7 @@ { "event": { "delta": { - "partial_json": "{\"cwd\":\"/workspace\"}", + "partial_json": "{\"command\":\"pwd\",\"cwd\":\"/workspace\"}", "type": "input_json_delta" }, "index": 1, @@ -165,16 +163,7 @@ "delivery": "best_effort", "kind": "tool.call.updated", "payload": { - "rawInput": "{\"command\":\"pwd\"}", - "status": "running", - "toolCallId": "tool-1" - } - }, - { - "delivery": "best_effort", - "kind": "tool.call.updated", - "payload": { - "rawInput": "{\"cwd\":\"/workspace\"}", + "rawInputDelta": "{\"command\":\"pwd\",\"cwd\":\"/workspace\"}", "status": "running", "toolCallId": "tool-1" } diff --git a/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json b/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json index 35e9c79..1de650d 100644 --- a/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json +++ b/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json @@ -70,7 +70,7 @@ "kind": "tool.call.updated", "payload": { "messageId": "", - "rawOutput": "hello", + "rawOutputDelta": "hello", "status": "running", "toolCallId": "cmd-1" } @@ -80,7 +80,7 @@ "kind": "tool.call.updated", "payload": { "messageId": "", - "rawOutput": " world", + "rawOutputDelta": " world", "status": "running", "toolCallId": "cmd-1" } diff --git a/tests/openai-app-server-client.test.ts b/tests/openai-app-server-client.test.ts index 58840a2..e9c2f29 100644 --- a/tests/openai-app-server-client.test.ts +++ b/tests/openai-app-server-client.test.ts @@ -24,7 +24,7 @@ const initializeResult = { codexHome: "/tmp/openai-home", platformFamily: "unix", platformOs: "linux", - userAgent: "test-app-server/0.150.1", + userAgent: "test-app-server/0.151.0", } as const; const initializeResultJson = JSON.stringify(initializeResult); diff --git a/tests/openai-app-server-event-bridge-items.test.ts b/tests/openai-app-server-event-bridge-items.test.ts index cf39063..cc0e76a 100644 --- a/tests/openai-app-server-event-bridge-items.test.ts +++ b/tests/openai-app-server-event-bridge-items.test.ts @@ -405,10 +405,11 @@ describe("OpenAi app-server event bridge", () => { test("replays a full task snapshot before committing its active set", async () => { const harness = createHarness({ failReasonOnce: "driver.openai.item.completed" }); + const nativeTaskId = "agent".repeat(100); const notification = { item: { agentPath: "/root/worker", - agentThreadId: "agent-1", + agentThreadId: nativeTaskId, id: "activity-1", kind: "started", type: "subAgentActivity", @@ -428,9 +429,13 @@ describe("OpenAi app-server event bridge", () => { ); expect(attempts).toHaveLength(2); expect(attempts[0]!.events).toEqual(attempts[1]!.events); - expect(harness.events().filter((event) => event.kind === "agent.tasks.replaced")).toMatchObject( - [{ payload: { tasks: [{ taskId: "agent-1" }] } }], - ); + const taskUpdate = harness.events().find((event) => event.kind === "agent.task.updated"); + const taskSnapshot = harness.events().find((event) => event.kind === "agent.tasks.replaced"); + const taskId = taskUpdate === undefined ? null : readEventPayloadString(taskUpdate, "taskId"); + expect(taskId).toMatch(/^openai-task:[\da-f]{64}$/); + expect(taskSnapshot).toMatchObject({ + payload: { tasks: [{ taskId }] }, + }); }); test("replays a plan delta after delivery rejection without duplicating content", async () => { diff --git a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts index 93c3716..3861830 100644 --- a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts +++ b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts @@ -579,7 +579,7 @@ describe("OpenAi app-server event bridge", () => { kind: "tool.call.updated", payload: { messageId: assistantMessageId, - rawOutput: "hello", + rawOutputDelta: "hello", status: "running", toolCallId: "cmd-1", }, @@ -588,13 +588,36 @@ describe("OpenAi app-server event bridge", () => { kind: "tool.call.updated", payload: { messageId: assistantMessageId, - rawOutput: " world", + rawOutputDelta: " world", status: "running", toolCallId: "cmd-1", }, }, ]); expect(events().some((event) => event.kind === "item.updated")).toBe(false); + + await bridge.handleNotification(context, "item/completed", { + item: { + aggregatedOutput: "hello world", + command: "printf 'hello world'", + id: "cmd-1", + status: "completed", + type: "commandExecution", + }, + threadId: "thread-1", + turnId: "turn-1", + }); + + const terminal = events() + .filter( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "toolCallId") === "cmd-1" && + readEventPayloadString(event, "status") === "completed", + ) + .at(-1); + expect(terminal?.payload).toMatchObject({ rawOutput: "hello world" }); + expect(terminal?.payload).not.toHaveProperty("rawOutputDelta"); }); test("turn plan updates map to the session plan custom event", async () => { diff --git a/tests/openai-app-server-event-bridge-terminal.test.ts b/tests/openai-app-server-event-bridge-terminal.test.ts index 9b5a67f..916b225 100644 --- a/tests/openai-app-server-event-bridge-terminal.test.ts +++ b/tests/openai-app-server-event-bridge-terminal.test.ts @@ -46,6 +46,19 @@ function expectRunFinalReferences( expect(messageId).not.toBeNull(); expect(snapshots[0]?.kind).toBe("message.added"); expect(content).toBe(expectedContent); + const lastSnapshotIndex = events.findLastIndex( + (event) => + readEventPayloadString(event, "messageId") === messageId && + (event.kind === "message.added" || event.kind === "message.delta"), + ); + const sealIndex = events.findIndex( + (event, index) => + index > lastSnapshotIndex && + event.kind === "message.completed" && + readEventPayloadString(event, "messageId") === messageId, + ); + expect(sealIndex).toBeGreaterThan(lastSnapshotIndex); + expect(events.findIndex((event) => event === terminal)).toBeGreaterThan(sealIndex); expect(terminal!.payload).not.toHaveProperty("finalMessageText"); } @@ -91,7 +104,7 @@ function createPublisherHarness( delivered.push(...acceptedEvents); return { accepted: acceptedEvents.map((event) => ({ - eventId: event.sourceEventId, + eventId: event.sourceEventId!, seq: nextSeq++, type: event.kind, })), @@ -710,16 +723,21 @@ describe("OpenAi app-server event bridge", () => { expect(events.map((event) => readEventPayloadString(event, "content")).join("")).toBe(large); expect(events.length).toBeGreaterThan(1); expect(events.every((event) => event.delivery === "lossless")).toBe(true); + const messageIds = new Set(events.map((event) => readEventPayloadString(event, "messageId"))); + expect(messageIds.size).toBe(events.length); + expect([...messageIds].every(isDriverId)).toBe(true); } const strictReview = delivered.find( (event) => readEventPayloadString(event, "subtype") === "strict_review_required", ); expect(strictReview?.delivery).toBe("lossless"); expect(strictReview?.payload).toMatchObject({ startedAtMs: 1 }); + expect(isDriverId(readEventPayloadString(strictReview!, "messageId"))).toBe(true); const worldWritable = delivered.find( (event) => readEventPayloadString(event, "subtype") === "windows_world_writable_warning", ); expect(worldWritable?.payload).toMatchObject({ extraCount: 3, samplePaths: [] }); + expect(isDriverId(readEventPayloadString(worldWritable!, "messageId"))).toBe(true); expect(delivered.every((event) => Buffer.byteLength(JSON.stringify(event)) < 1_048_576)).toBe( true, ); @@ -736,7 +754,16 @@ describe("OpenAi app-server event bridge", () => { turn: { completedAt: 2, durationMs: 1, - error: { additionalDetails: large, codexErrorInfo: null, message: large }, + error: { + additionalDetails: large, + codexErrorInfo: null, + message: large, + misalignment: { + detailedExplanation: large, + errorType: large, + steer: { message: large }, + }, + }, id: "turn-1", items: [], itemsView: "notLoaded", @@ -750,19 +777,23 @@ describe("OpenAi app-server event bridge", () => { await bridge.handleNotification(context, notification!.method, notification!.params); await expect(trackedTurn).rejects.toThrow("was omitted"); - const terminal = delivered.at(-1); - expect(terminal).toMatchObject({ + const terminal = delivered.at(-1)!; + const [canonical] = toDriverEventEnvelopes(driverBootPayload, terminal, DRIVER_TEST_IDS.runId); + expect(canonical?.event).toMatchObject({ kind: "run.failed", payload: { error: { details: { additionalDetailsUtf8Bytes: 1_100_000, messageUtf8Bytes: 1_100_000, + misalignmentDetailedExplanationUtf8Bytes: 1_100_000, + misalignmentErrorTypeUtf8Bytes: 1_100_000, + misalignmentSteerMessageUtf8Bytes: 1_100_000, }, }, }, }); - expect(Buffer.byteLength(JSON.stringify(terminal))).toBeLessThan(1_048_576); + expect(Buffer.byteLength(JSON.stringify(canonical))).toBeLessThan(1_048_576); }); test("removes OpenAI private citation markup from streamed and final assistant text", async () => { diff --git a/tests/openai-app-server-event-bridge-transcript.test.ts b/tests/openai-app-server-event-bridge-transcript.test.ts index dfe7531..ed82181 100644 --- a/tests/openai-app-server-event-bridge-transcript.test.ts +++ b/tests/openai-app-server-event-bridge-transcript.test.ts @@ -309,6 +309,72 @@ describe("OpenAi app-server event bridge", () => { ]); }); + test("classifies exhausted streaming rate limits as recoverable", async () => { + const { bridge, context, events } = createHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { + error: { + codexErrorInfo: "rateLimitExceeded", + message: "Streaming rate limit exceeded.", + }, + id: "turn-1", + status: "failed", + }, + }); + + await expect(trackedTurn).rejects.toThrow("Streaming rate limit exceeded."); + expect(events().find((event) => event.kind === "run.failed")).toMatchObject({ + payload: { + error: { + details: { codexErrorInfo: "rateLimitExceeded" }, + retryable: true, + }, + recoverable: true, + }, + }); + }); + + test("preserves actionable misalignment details without making the failure retryable", async () => { + const { bridge, context, events } = createHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); + + await bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { + error: { + codexErrorInfo: "misalignmentPolicyViolation", + message: "The request was blocked by policy.", + misalignment: { + detailedExplanation: "The request needs a narrower authorized scope.", + errorType: "scope_mismatch", + steer: { message: "Continue only within the authorized repository." }, + }, + }, + id: "turn-1", + status: "failed", + }, + }); + + await expect(trackedTurn).rejects.toThrow("The request was blocked by policy."); + expect(events().find((event) => event.kind === "run.failed")).toMatchObject({ + payload: { + error: { + details: { + codexErrorInfo: "misalignmentPolicyViolation", + misalignmentDetailedExplanation: "The request needs a narrower authorized scope.", + misalignmentErrorType: "scope_mismatch", + misalignmentSteerMessage: "Continue only within the authorized repository.", + }, + retryable: false, + }, + recoverable: false, + }, + }); + }); + test("thread systemError waits for the authoritative failed turn", async () => { const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); diff --git a/tests/openai-app-server-provider-fixtures.test.ts b/tests/openai-app-server-provider-fixtures.test.ts index 7b45835..f75a5f2 100644 --- a/tests/openai-app-server-provider-fixtures.test.ts +++ b/tests/openai-app-server-provider-fixtures.test.ts @@ -203,7 +203,7 @@ function createThreadFixture(id = "thread-1") { agentNickname: null, agentRole: null, canAcceptDirectInput: true, - cliVersion: "0.150.1", + cliVersion: "0.151.0", createdAt: 1_700_000_000, cwd: "/workspace", ephemeral: false, @@ -480,7 +480,7 @@ describe("OpenAI app-server provider fixtures", () => { codexHome: "/tmp/openai-home", platformFamily: "unix", platformOs: "linux", - userAgent: "test-app-server/0.150.1", + userAgent: "test-app-server/0.151.0", }; delete response[missing]; @@ -493,7 +493,7 @@ describe("OpenAI app-server provider fixtures", () => { codexHome: "/tmp/openai-home", platformFamily: "unix", platformOs: "linux", - userAgent: "test-app-server/0.150.1", + userAgent: "test-app-server/0.151.0", }; expect(CLIENT_RESULT_SCHEMAS.initialize.parse(response)).toEqual(response); @@ -579,6 +579,7 @@ describe("OpenAI app-server provider fixtures", () => { additionalDetails: "HTTP 502 from upstream.", codexErrorInfo: { responseStreamDisconnected: { httpStatusCode: 502 } }, message: "Response stream disconnected.", + misalignment: null, }, status: "failed", }; @@ -621,7 +622,7 @@ describe("OpenAI app-server provider fixtures", () => { willRetry: false, }), ).toEqual({ - error: { additionalDetails: null, message: "Provider failed." }, + error: { additionalDetails: null, message: "Provider failed.", misalignment: null }, threadId: "thread-1", turnId: "turn-1", willRetry: false, @@ -1078,15 +1079,29 @@ describe("OpenAI app-server provider fixtures", () => { id: "hook-echo-1", type: "hookPrompt", }, + { + id: "function-output-echo-1", + name: "lookup", + namespace: "project", + output: "provider echo", + type: "functionCallOutput", + }, ] as const) { - await dispatchProviderNotification( - { - method: "item/completed", - params: { completedAtMs: 3, item, threadId: "thread-1", turnId: "turn-1" }, - }, - bridge, - context, - ); + for (const method of ["item/started", "item/completed"] as const) { + await dispatchProviderNotification( + { + method, + params: { + ...(method === "item/started" ? { startedAtMs: 2 } : { completedAtMs: 3 }), + item, + threadId: "thread-1", + turnId: "turn-1", + }, + }, + bridge, + context, + ); + } } expect(events()).toHaveLength(eventCount); }); @@ -1155,7 +1170,7 @@ describe("OpenAI app-server provider fixtures", () => { }, ); - test("publishes sub-agent activity from a terminal turn snapshot", async () => { + test("publishes sub-agent activity and ignores tool-output echo from a terminal snapshot", async () => { const { bridge, context, events } = createHarness(); await dispatchProviderNotification( @@ -1166,6 +1181,13 @@ describe("OpenAI app-server provider fixtures", () => { turn: { id: "turn-snapshot", items: [ + { + id: "function-output-snapshot", + name: "lookup", + namespace: null, + output: "provider echo", + type: "functionCallOutput", + }, { agentPath: "/root/replayed-worker", agentThreadId: "agent-replayed", diff --git a/tests/openai-app-server-startup.test.ts b/tests/openai-app-server-startup.test.ts index 651c775..3b783af 100644 --- a/tests/openai-app-server-startup.test.ts +++ b/tests/openai-app-server-startup.test.ts @@ -27,7 +27,7 @@ const initializeResultJson = JSON.stringify({ codexHome: "/tmp/openai-home", platformFamily: "unix", platformOs: "linux", - userAgent: "test-app-server/0.150.1", + userAgent: "test-app-server/0.151.0", }); function eventPayloadStatus(event: DriverEventInput): unknown { @@ -211,7 +211,7 @@ const thread = { ephemeral: false, section: null, sectionEnteredAt: null, - historyMode: "legacy", + historyMode: "paginated", modelProvider: "openai", createdAt: 0, updatedAt: 0, @@ -219,7 +219,7 @@ const thread = { status: { type: "idle" }, path: null, cwd: ${JSON.stringify(directory)}, - cliVersion: "0.150.1", + cliVersion: "0.151.0", source: "appServer", canAcceptDirectInput: true, threadSource: null, @@ -527,6 +527,7 @@ process.stdin.on("data", (chunk) => { } return { accepted: input.events.map((event, index) => ({ + eventId: event.sourceEventId!, seq: index + 1, type: event.kind, })), @@ -636,6 +637,8 @@ describe("OpenAI app-server startup", () => { params?: { approvalPolicy?: string; cwd?: string; + excludeTurns?: boolean; + historyMode?: string; input?: unknown; model?: string; threadId?: string; @@ -649,6 +652,18 @@ describe("OpenAI app-server startup", () => { ) .map((request) => request.params?.approvalPolicy), ).toEqual(["untrusted", "untrusted", "untrusted"]); + expect(requests.find((request) => request.method === "thread/resume")?.params).toMatchObject({ + excludeTurns: true, + }); + expect( + requests.find((request) => request.method === "thread/resume")?.params, + ).not.toHaveProperty("historyMode"); + expect(requests.find((request) => request.method === "thread/start")?.params).toMatchObject({ + historyMode: "paginated", + }); + expect( + requests.find((request) => request.method === "thread/start")?.params, + ).not.toHaveProperty("excludeTurns"); expect(requests.find((request) => request.method === "turn/start")?.params).toEqual({ approvalPolicy: "untrusted", cwd: harness.payload.execution.session.cwd, diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index 9cc8dd0..56754c6 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -8,7 +8,12 @@ import { protocolVersionSchema as protocolVersionSchemaFromContract, sessionSnapshotSchema as sessionSnapshotSchemaFromContract, } from "@mosoo/agent-driver/contract"; -import { parseDriverEventEnvelope as parseDriverEventEnvelopeFromSubpath } from "@mosoo/agent-driver/events"; +import { + RUNTIME_EVENT_KINDS as RUNTIME_EVENT_KINDS_FROM_SUBPATH, + RUNTIME_EVENT_SCHEMA_VERSION as RUNTIME_EVENT_SCHEMA_VERSION_FROM_SUBPATH, + parseDriverEventEnvelope as parseDriverEventEnvelopeFromSubpath, + toRuntimeEventInput as toRuntimeEventInputFromSubpath, +} from "@mosoo/agent-driver/events"; import { AGENT_DRIVER_PROVIDER_REGISTRY, AgentDriverKernelCore, @@ -62,13 +67,16 @@ describe("public API", () => { test("imports public subpath entries without process side effects", () => { const heartbeatReason = "ping" satisfies DriverHeartbeatInputFromOrpcSubpath["reason"]; - expect(DRIVER_PROTOCOL_VERSION_FROM_BOOT_SUBPATH).toBe(2); + expect(DRIVER_PROTOCOL_VERSION_FROM_BOOT_SUBPATH).toBe(3); expect(PROTOCOL_VERSION_FROM_CONTRACT).toBe(2); expect(protocolVersionSchemaFromContract.parse(2)).toBe(2); expect(sessionSnapshotSchemaFromContract.parse).toBeFunction(); expect(createCmaHttpHandlerFromSubpath).toBe(createCmaHttpHandler); expect(CmaSdkClientFromSubpath).toBe(CmaSdkClient); expect(parseDriverEventEnvelopeFromSubpath).toBeFunction(); + expect(toRuntimeEventInputFromSubpath).toBeFunction(); + expect(RUNTIME_EVENT_SCHEMA_VERSION_FROM_SUBPATH).toBe("2026-08-29"); + expect(RUNTIME_EVENT_KINDS_FROM_SUBPATH).toContain("agent.tasks.replaced"); expect(heartbeatReason).toBe("ping"); expect(parseDriverHeartbeatInputFromOrpcSubpath({ at: "now", pid: 1, reason: "ping" })).toEqual( { @@ -82,7 +90,7 @@ describe("public API", () => { capabilities: [], driverVersion: "0.1.0", pid: 1, - protocolVersion: 2, + protocolVersion: DRIVER_PROTOCOL_VERSION_FROM_BOOT_SUBPATH, runtime: "openai-runtime", startedAt: "now", }), diff --git a/tests/remote-http-mcp-executor.test.ts b/tests/remote-http-mcp-executor.test.ts index 4f26998..a3cfcb2 100644 --- a/tests/remote-http-mcp-executor.test.ts +++ b/tests/remote-http-mcp-executor.test.ts @@ -1,10 +1,13 @@ -import { expect, test } from "bun:test"; +import { expect, spyOn, test } from "bun:test"; +import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/client"; import type { CredentialId, McpServerId } from "../src/protocol/boot"; import type { DriverStartInput } from "../src/protocol/start"; import type { McpExecuteCommand } from "../src/runtime-command"; +import { createDisabledLogger } from "../src/observability"; import { prepareRemoteHttpMcpCommand } from "../src/runtimes/mcp/remote-http-mcp-executor"; -import { driverStartInput } from "./driver-boot-payload-fixture"; +import { settlePromiseWithTimeout } from "../src/utils/async"; +import { DRIVER_TEST_IDS, driverStartInput } from "./driver-boot-payload-fixture"; const MCP_SERVER_ID = "01J00000000000000000000020" as McpServerId; const MCP_CREDENTIAL_ID = "01J00000000000000000000021" as CredentialId; @@ -40,6 +43,7 @@ function command(requestId: string): McpExecuteCommand { commandId: `command-${requestId}`, kind: "mcp.execute", requestId, + runId: DRIVER_TEST_IDS.runId, serverId: MCP_SERVER_ID, toolCallId: `tool-${requestId}`, toolName: "lookup", @@ -51,14 +55,85 @@ async function execute(proxyUrl: string, requestId: string, signal = new AbortCo payload(proxyUrl), command(requestId), signal, + createDisabledLogger(), ); const result = await prepared.execute({ + attempt: 1, effectId: `effect-${requestId}`, idempotencyKey: `key-${requestId}`, + kind: "claimed", }); return result; } +function sessionServer( + onDelete: (request: Request, attempt: number) => Response | Promise, +) { + let deleteRequests = 0; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + if (request.method === "DELETE") { + deleteRequests += 1; + return onDelete(request, deleteRequests); + } + if (request.method === "GET") { + return new Response(null, { status: 405 }); + } + + const message = (await request.json()) as { + id?: unknown; + method?: string; + params?: { protocolVersion?: string }; + }; + if (message.method === "server/discover") { + return new Response("not found", { status: 404 }); + } + if (message.method === "initialize") { + return Response.json( + { + id: message.id, + jsonrpc: "2.0", + result: { + capabilities: { tools: {} }, + protocolVersion: message.params?.protocolVersion, + serverInfo: { name: "test-mcp", version: "1" }, + }, + }, + { headers: { "mcp-session-id": "cleanup-session" } }, + ); + } + if (message.method === "notifications/initialized") { + return new Response(null, { status: 202 }); + } + if (message.method === "tools/call") { + return Response.json({ + id: message.id, + jsonrpc: "2.0", + result: { content: [{ text: "ok", type: "text" }] }, + }); + } + return new Response("unexpected request", { status: 400 }); + }, + }); + + return { + deleteRequests: () => deleteRequests, + server, + url: `http://${server.hostname}:${server.port}/mcp`, + }; +} + +function prepareSession(proxyUrl: string, requestId: string, logger = createDisabledLogger()) { + return prepareRemoteHttpMcpCommand( + payload(proxyUrl), + command(requestId), + new AbortController().signal, + logger, + ); +} + test("classifies typed MCP HTTP failures by status", async () => { const unauthorizedHeaders: Array = []; const server = Bun.serve({ @@ -96,6 +171,7 @@ test("classifies typed MCP HTTP failures by status", async () => { }); test("auto-negotiates the latest MCP era and falls back to legacy", async () => { + const idempotencyKeys: unknown[] = []; const methods = new Map(); const protocolHeaders: Array = []; const server = Bun.serve({ @@ -110,7 +186,10 @@ test("auto-negotiates the latest MCP era and falls back to legacy", async () => const message = (await request.json()) as { id?: unknown; method?: string; - params?: { protocolVersion?: string }; + params?: { + _meta?: Record; + protocolVersion?: string; + }; }; const method = message.method ?? "unknown"; methods.set(route, [...(methods.get(route) ?? []), method]); @@ -147,6 +226,7 @@ test("auto-negotiates the latest MCP era and falls back to legacy", async () => } if (method === "tools/call") { + idempotencyKeys.push(message.params?._meta?.["io.mosoo/idempotency-key"]); protocolHeaders.push(request.headers.get("mcp-protocol-version")); if (route === "scope") { return new Response("insufficient scope", { @@ -241,6 +321,13 @@ test("auto-negotiates the latest MCP era and falls back to legacy", async () => "tools/call", ]); expect(methods.get("scope")).toEqual(["server/discover", "tools/call"]); + expect(idempotencyKeys).toEqual([ + "key-modern", + "key-legacy", + "key-mixed", + "key-annotated", + "key-scope", + ]); expect(protocolHeaders).toEqual([ "2026-07-28", expect.stringMatching(/^2025-/), @@ -253,10 +340,77 @@ test("auto-negotiates the latest MCP era and falls back to legacy", async () => } }); -test("terminates an established MCP session after cancellation", async () => { +test.each(["malformed-json", "oversized-content-length"] as const)( + "terminates a session exactly once after an initialize %s failure", + async (mode) => { + const deletes: Array<{ + authorization: string | null; + protocolVersion: string | null; + sessionId: string | null; + toolCallId: string | null; + }> = []; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + if (request.method === "DELETE") { + deletes.push({ + authorization: request.headers.get("authorization"), + protocolVersion: request.headers.get("mcp-protocol-version"), + sessionId: request.headers.get("mcp-session-id"), + toolCallId: request.headers.get("x-mosoo-tool-call-id"), + }); + return new Response(null, { status: 200 }); + } + if (request.method === "GET") { + return new Response(null, { status: 405 }); + } + + const message = (await request.json()) as { method?: string }; + if (message.method === "server/discover") { + return new Response("not found", { status: 404 }); + } + if (message.method === "initialize") { + return new Response( + mode === "malformed-json" ? "{" : new Uint8Array(8 * 1_024 * 1_024 + 1), + { + headers: { + "content-type": "application/json", + "mcp-session-id": `${mode}-session`, + }, + status: 200, + }, + ); + } + return new Response(null, { status: 202 }); + }, + }); + + try { + await expect( + prepareSession(`http://${server.hostname}:${server.port}/mcp`, mode), + ).rejects.toThrow( + mode === "malformed-json" ? "JSON Parse error" : "MCP response exceeds 8388608 bytes", + ); + expect(deletes).toEqual([ + { + authorization: "Bearer test-grant", + protocolVersion: LATEST_PROTOCOL_VERSION, + sessionId: `${mode}-session`, + toolCallId: `tool-${mode}`, + }, + ]); + } finally { + await server.stop(true); + } + }, +); + +test("does not interrupt a committed MCP call when the prepare signal is cancelled", async () => { const callStarted = Promise.withResolvers(); - const cancelled = Promise.withResolvers(); + const releaseCall = Promise.withResolvers(); const sessionDeleted = Promise.withResolvers(); + let cancellationNotifications = 0; const server = Bun.serve({ hostname: "127.0.0.1", port: 0, @@ -300,24 +454,17 @@ test("terminates an established MCP session after cancellation", async () => { } if (message.method === "notifications/cancelled") { - cancelled.resolve(); + cancellationNotifications += 1; return new Response(null, { status: 202 }); } if (message.method === "tools/call") { callStarted.resolve(); - if (!request.signal.aborted) { - await Promise.race([ - cancelled.promise, - new Promise((resolve) => - request.signal.addEventListener("abort", () => resolve(), { once: true }), - ), - ]); - } + await releaseCall.promise; return Response.json({ id: message.id, jsonrpc: "2.0", - result: { content: [{ text: "cancelled", type: "text" }] }, + result: { content: [{ text: "committed", type: "text" }] }, }); } @@ -334,10 +481,188 @@ test("terminates an established MCP session after cancellation", async () => { ); await callStarted.promise; controller.abort(new Error("cancel requested")); + releaseCall.resolve(); - await expect(execution).rejects.toThrow(); + await expect(execution).resolves.toMatchObject({ outputText: "committed" }); expect(await sessionDeleted.promise).toBe("cancelled-session"); + expect(cancellationNotifications).toBe(0); } finally { + releaseCall.resolve(); await server.stop(true); } }); + +test("retries a settled MCP session termination failure once", async () => { + const harness = sessionServer( + (_request, attempt) => new Response(null, { status: attempt === 1 ? 503 : 200 }), + ); + + try { + const prepared = await prepareSession(harness.url, "cleanup-retry"); + await prepared[Symbol.asyncDispose](); + expect(harness.deleteRequests()).toBe(2); + } finally { + await harness.server.stop(true); + } +}); + +test("closes locally and reports an exhausted MCP session termination", async () => { + const harness = sessionServer(() => new Response(null, { status: 503 })); + const logger = createDisabledLogger(); + const warn = spyOn(logger, "warn"); + + try { + const prepared = await prepareSession(harness.url, "cleanup-failed", logger); + await expect(prepared[Symbol.asyncDispose]()).resolves.toBeUndefined(); + expect(harness.deleteRequests()).toBe(2); + expect(warn).toHaveBeenCalledWith( + "driver.mcp.session-termination.failed", + expect.objectContaining({ status: "failed" }), + ); + } finally { + warn.mockRestore(); + await harness.server.stop(true); + } +}); + +test("does not overlap MCP session termination after its deadline", async () => { + const releaseDelete = Promise.withResolvers(); + const harness = sessionServer(async () => { + await releaseDelete.promise; + return new Response(null, { status: 200 }); + }); + + try { + const prepared = await prepareSession(harness.url, "cleanup-timeout"); + const startedAt = performance.now(); + await prepared[Symbol.asyncDispose](); + expect(performance.now() - startedAt).toBeGreaterThanOrEqual(1_900); + expect(harness.deleteRequests()).toBe(1); + } finally { + releaseDelete.resolve(); + await harness.server.stop(true); + } +}, 10_000); + +test("joins concurrent MCP disposal into one cleanup transaction", async () => { + const harness = sessionServer(() => new Response(null, { status: 200 })); + + try { + const prepared = await prepareSession(harness.url, "cleanup-joined"); + await Promise.all([ + prepared[Symbol.asyncDispose](), + prepared[Symbol.asyncDispose](), + prepared[Symbol.asyncDispose](), + ]); + expect(harness.deleteRequests()).toBe(1); + await expect( + prepared.execute({ + attempt: 1, + effectId: "late-effect", + idempotencyKey: "late-key", + kind: "claimed", + }), + ).rejects.toThrow("can only be executed once"); + } finally { + await harness.server.stop(true); + } +}); + +test.each(["content-length", "stream"] as const)( + "rejects an oversized MCP %s response before SDK parsing", + async (mode) => { + const responseBytes = 8 * 1_024 * 1_024 + 1; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + const message = (await request.json()) as { id?: unknown; method?: string }; + if (message.method === "server/discover") { + return Response.json({ + id: message.id, + jsonrpc: "2.0", + result: { + capabilities: { tools: {} }, + supportedVersions: ["2026-07-28"], + }, + }); + } + if (message.method === "tools/call") { + if (mode === "content-length") { + return new Response(new Uint8Array(responseBytes), { + headers: { + "content-length": String(responseBytes), + "content-type": "application/json", + }, + }); + } + + let remaining = responseBytes; + return new Response( + new ReadableStream({ + pull(controller) { + if (remaining === 0) { + controller.close(); + return; + } + const chunk = new Uint8Array(Math.min(1_024 * 1_024, remaining)); + remaining -= chunk.byteLength; + controller.enqueue(chunk); + }, + }), + { headers: { "content-type": "application/json" } }, + ); + } + return new Response("unexpected request", { status: 400 }); + }, + }); + + try { + await expect( + execute(`http://${server.hostname}:${server.port}/oversized`, `oversized-${mode}`), + ).rejects.toThrow("MCP response exceeds 8388608 bytes"); + } finally { + await server.stop(true); + } + }, + 10_000, +); + +test("does not await cancellation of an oversized Content-Length body", async () => { + let cancelCalled = false; + const fetch = spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + new ReadableStream({ + cancel() { + cancelCalled = true; + return new Promise(() => {}); + }, + }), + { + headers: { + "content-length": String(8 * 1_024 * 1_024 + 1), + "content-type": "application/json", + }, + }, + ), + ); + + try { + const outcome = await settlePromiseWithTimeout( + prepareSession("https://mcp.invalid/oversized", "oversized-cancel"), + { + label: "oversized MCP Content-Length rejection", + timeoutMs: 1_000, + }, + ); + + expect(outcome.status).toBe("failed"); + if (outcome.status === "failed") { + expect(outcome.error).toBeInstanceOf(Error); + expect((outcome.error as Error).message).toContain("MCP response exceeds 8388608 bytes"); + } + expect(cancelCalled).toBe(true); + } finally { + fetch.mockRestore(); + } +}); From 699e2fd587ae0f1ff45d81510853ca00f7addfa1 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Sun, 30 Aug 2026 09:54:20 +0000 Subject: [PATCH 09/20] fix(cma): bound stalled SSE cancellation Do not wait indefinitely for a non-compliant ReadableStream cancel promise after abort or iterator return. Keep cancellation best effort and cover both cooperative and permanently stalled underlying sources. --- src/surfaces/cma-sdk/sse-bytes-decoder.ts | 2 +- tests/cma-sdk.test.ts | 83 ++++++++++++----------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/surfaces/cma-sdk/sse-bytes-decoder.ts b/src/surfaces/cma-sdk/sse-bytes-decoder.ts index 4a0dd9c..27676d3 100644 --- a/src/surfaces/cma-sdk/sse-bytes-decoder.ts +++ b/src/surfaces/cma-sdk/sse-bytes-decoder.ts @@ -177,7 +177,7 @@ export async function* decodeCmaSseBytes( signal?.removeEventListener("abort", onAbort); if (!completed) { - await cancel(); + void cancel(); } reader.releaseLock(); diff --git a/tests/cma-sdk.test.ts b/tests/cma-sdk.test.ts index a610eb9..a20399e 100644 --- a/tests/cma-sdk.test.ts +++ b/tests/cma-sdk.test.ts @@ -429,46 +429,53 @@ describe("CMA SDK client", () => { expect(canceled).toBe(true); }); - test("return interrupts a pending SSE read and waits for cleanup", async () => { - const cleanup = Promise.withResolvers(); - const readEntered = Promise.withResolvers(); - let fetchSignal: AbortSignal | undefined; - const client = new CmaSdkClient({ - baseUrl: "https://driver.test", - fetch: async (_input, init) => { - fetchSignal = init?.signal ?? undefined; - return new Response( - new ReadableStream( - { - cancel() { - cleanup.resolve(); - }, - pull() { - readEntered.resolve(); + test.each(["settles", "stalls"] as const)( + "return interrupts a pending SSE read when underlying cancellation %s", + async (cancelMode) => { + const cancelEntered = Promise.withResolvers(); + const readEntered = Promise.withResolvers(); + let fetchSignal: AbortSignal | undefined; + const client = new CmaSdkClient({ + baseUrl: "https://driver.test", + fetch: async (_input, init) => { + fetchSignal = init?.signal ?? undefined; + return new Response( + new ReadableStream( + { + cancel() { + cancelEntered.resolve(); + return cancelMode === "stalls" ? new Promise(() => {}) : undefined; + }, + pull() { + readEntered.resolve(); + }, }, - }, - { highWaterMark: 0 }, - ), - ); - }, - }); - const iterator = client.streamSessionEvents("session-1")[Symbol.asyncIterator](); - const pending = iterator.next(); - await readEntered.promise; + { highWaterMark: 0 }, + ), + ); + }, + }); + const iterator = client.streamSessionEvents("session-1")[Symbol.asyncIterator](); + const pending = iterator.next(); + await readEntered.promise; - const returned = iterator.return?.(); + const returned = iterator.return?.(); - if (!returned) { - throw new Error("Expected the CMA stream iterator to support return()."); - } + if (!returned) { + throw new Error("Expected the CMA stream iterator to support return()."); + } - await expect( - promiseWithTimeout(pending, { label: "pending CMA stream read", timeoutMs: 100 }), - ).rejects.toMatchObject({ name: "AbortError" }); - await expect( - promiseWithTimeout(returned, { label: "CMA stream return", timeoutMs: 100 }), - ).resolves.toMatchObject({ done: true }); - await promiseWithTimeout(cleanup.promise, { label: "CMA stream cleanup", timeoutMs: 100 }); - expect(fetchSignal?.aborted).toBe(true); - }); + await expect( + promiseWithTimeout(pending, { label: "pending CMA stream read", timeoutMs: 100 }), + ).rejects.toMatchObject({ name: "AbortError" }); + await expect( + promiseWithTimeout(returned, { label: "CMA stream return", timeoutMs: 100 }), + ).resolves.toMatchObject({ done: true }); + await promiseWithTimeout(cancelEntered.promise, { + label: "CMA stream cancellation", + timeoutMs: 100, + }); + expect(fetchSignal?.aborted).toBe(true); + }, + ); }); From b46812a823398b3eadd0b83caa4041e06b5008b5 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Sun, 30 Aug 2026 10:06:36 +0000 Subject: [PATCH 10/20] test(runtime): assert process-tree termination --- tests/openai-app-server-client.test.ts | 60 ++++++++++++-------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/tests/openai-app-server-client.test.ts b/tests/openai-app-server-client.test.ts index e9c2f29..a8c5f6f 100644 --- a/tests/openai-app-server-client.test.ts +++ b/tests/openai-app-server-client.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, spyOn, test } from "bun:test"; import type { ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; +import { readFileSync } from "node:fs"; import { chmod, mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -28,6 +29,28 @@ const initializeResult = { } as const; const initializeResultJson = JSON.stringify(initializeResult); +function isProcessRunning(pid: number): boolean { + try { + process.kill(pid, 0); + return ( + process.platform !== "linux" || + !/^\d+ \(.*\) Z /.test(readFileSync(`/proc/${pid}/stat`, "utf8")) + ); + } catch { + return false; + } +} + +async function expectProcessExited(pid: number, timeoutMs = 3_000): Promise { + const deadline = Date.now() + timeoutMs; + + while (isProcessRunning(pid) && Date.now() < deadline) { + await Bun.sleep(20); + } + + expect(isProcessRunning(pid)).toBe(false); +} + async function createClientHarness( script: (directory: string) => string, handleNotification: (method: ServerNotificationMethod) => Promise = async () => {}, @@ -569,21 +592,7 @@ setInterval(() => {}, 1000); } await expect(start).rejects.toThrow("process-tree watchdog"); - await expect( - settlePromiseWithTimeout( - (async () => { - for (;;) { - try { - process.kill(childPid, 0); - await Bun.sleep(5); - } catch { - return; - } - } - })(), - { label: "unsupervised app-server exit", timeoutMs: 250 }, - ), - ).resolves.toMatchObject({ status: "completed" }); + await expectProcessExited(childPid, 250); expect(harness.protocolErrors.some((error) => error.message.includes("watchdog"))).toBe( true, ); @@ -1322,11 +1331,9 @@ setInterval(() => {}, 1000); test("force kills the process group after the leader exits with inherited stdio open", async () => { const harness = await createClientHarness((directory) => { const descendantReadyPath = join(directory, "descendant-ready"); - const descendantTermPath = join(directory, "descendant-term"); const descendantScript = ` import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(descendantReadyPath)}, "ready"); -process.on("SIGTERM", () => writeFileSync(${JSON.stringify(descendantTermPath)}, "ignored")); setInterval(() => {}, 1000); `; @@ -1362,7 +1369,8 @@ process.stdin.on("data", (chunk) => { } await expect(harness.client.stop()).resolves.toBeUndefined(); - expect(await Bun.file(join(harness.directory, "descendant-term")).exists()).toBe(true); + await expectProcessExited(descendantPid); + descendantPid = 0; } finally { if (descendantPid > 0) { try { @@ -1489,21 +1497,7 @@ setInterval(() => {}, 1000); await harness.client.stop(); expect(await Bun.file(join(harness.directory, "nested-term")).exists()).toBe(true); - await expect( - settlePromiseWithTimeout( - (async () => { - for (;;) { - try { - process.kill(descendantPid, 0); - await Bun.sleep(5); - } catch { - return; - } - } - })(), - { label: "nested app-server descendant exit", timeoutMs: 250 }, - ), - ).resolves.toMatchObject({ status: "completed" }); + await expectProcessExited(descendantPid, 250); } finally { if (descendantPid > 0) { try { From 6ec9bd1ac7e20fb2761f7748155f1a0e62f01b27 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Sun, 30 Aug 2026 10:11:32 +0000 Subject: [PATCH 11/20] fix(cma): bound oversized response cancellation --- src/surfaces/cma-sdk/client.ts | 2 +- tests/cma-sdk.test.ts | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/surfaces/cma-sdk/client.ts b/src/surfaces/cma-sdk/client.ts index daa45fa..a8dc7f8 100644 --- a/src/surfaces/cma-sdk/client.ts +++ b/src/surfaces/cma-sdk/client.ts @@ -81,7 +81,7 @@ async function readResponseBody( const contentLength = Number(response.headers.get("content-length")); if (Number.isFinite(contentLength) && contentLength > maxResponseBytes) { - await response.body?.cancel().catch(() => undefined); + void response.body?.cancel().catch(() => undefined); throw responseTooLarge(maxResponseBytes); } diff --git a/tests/cma-sdk.test.ts b/tests/cma-sdk.test.ts index a20399e..639e30f 100644 --- a/tests/cma-sdk.test.ts +++ b/tests/cma-sdk.test.ts @@ -106,7 +106,12 @@ describe("CMA SDK client", () => { expect(fetchSignal?.aborted).toBe(true); }); - test("bounds and cancels JSON response bodies", async () => { + test.each([ + ["content-length", "cooperative"], + ["content-length", "stalled"], + ["stream", "cooperative"], + ["stream", "stalled"], + ] as const)("bounds %s JSON responses with %s cancellation", async (boundary, cancellation) => { let canceled = false; const client = new CmaSdkClient({ baseUrl: "https://driver.test", @@ -115,16 +120,23 @@ describe("CMA SDK client", () => { new ReadableStream({ cancel() { canceled = true; + return cancellation === "stalled" ? new Promise(() => {}) : undefined; }, start(controller) { controller.enqueue(new TextEncoder().encode('{"data":["too large"]}')); }, }), + boundary === "content-length" ? { headers: { "content-length": "9" } } : undefined, ), maxResponseBytes: 8, }); - await expect(client.listAgents()).rejects.toMatchObject({ + await expect( + promiseWithTimeout(client.listAgents(), { + label: "bounded CMA SDK response", + timeoutMs: 250, + }), + ).rejects.toMatchObject({ code: "CMA_SDK_RESPONSE_TOO_LARGE", }); expect(canceled).toBe(true); From 855c5ccfa1a637e00ac07283136df761f9c744ff Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Sun, 30 Aug 2026 10:24:08 +0000 Subject: [PATCH 12/20] fix(skills): bound download stream cancellation --- src/runtimes/skill-materialization.ts | 54 +++++---------------------- tests/skill-materialization.test.ts | 31 +++++++++++++-- 2 files changed, 37 insertions(+), 48 deletions(-) diff --git a/src/runtimes/skill-materialization.ts b/src/runtimes/skill-materialization.ts index 25ea5b2..6a55a2c 100644 --- a/src/runtimes/skill-materialization.ts +++ b/src/runtimes/skill-materialization.ts @@ -1,4 +1,3 @@ -import { Buffer } from "node:buffer"; import { createHash, randomUUID } from "node:crypto"; import { constants } from "node:fs"; import { mkdir, open, opendir, rename, rmdir, unlink } from "node:fs/promises"; @@ -11,6 +10,7 @@ import type { DriverResolvedSkill } from "../protocol/boot"; import type { DriverExecutionInput } from "../protocol/execution"; import { extractZipArchive } from "../skill-package"; import type { SkillArchiveExtractOptions, SkillPackageEntry } from "../skill-package"; +import { readBoundedStreamBytes } from "../utils/async"; import { assertDirectoryIdentity, closeFileHandles, @@ -1086,7 +1086,7 @@ async function downloadSkillPackage( const response = await fetch(skill.downloadUrl, { signal: requestSignal }); if (!response.ok) { - await response.body?.cancel().catch(() => {}); + void response.body?.cancel().catch(() => {}); throw new Error(`Failed to download skill package for ${skill.skillId}: ${response.status}.`); } if (response.body === null) { @@ -1099,54 +1099,20 @@ async function downloadSkillPackage( /^\d+$/.test(contentLength) && Number(contentLength) > MAX_SKILL_COMPRESSED_BYTES ) { - await response.body.cancel().catch(() => {}); + void response.body.cancel().catch(() => {}); throw new Error( `Compressed skill package exceeds the limit (${MAX_SKILL_COMPRESSED_BYTES} bytes).`, ); } - const reader = response.body.getReader(); - const chunks: Uint8Array[] = []; - const hash = createHash("sha256"); - let totalBytes = 0; - const onAbort = () => { - void reader.cancel(requestSignal.reason).catch(() => {}); - }; - requestSignal.addEventListener("abort", onAbort, { once: true }); - - try { - while (true) { - requestSignal.throwIfAborted(); - const { done, value } = await reader.read(); - requestSignal.throwIfAborted(); - - if (done) { - break; - } - - totalBytes += value.byteLength; - if (totalBytes > MAX_SKILL_COMPRESSED_BYTES) { - const error = new Error( - `Compressed skill package exceeds the limit (${MAX_SKILL_COMPRESSED_BYTES} bytes).`, - ); - await reader.cancel(error).catch(() => {}); - throw error; - } - - chunks.push(value); - hash.update(value); - } - } catch (error) { - await reader.cancel(error).catch(() => {}); - throw error; - } finally { - requestSignal.removeEventListener("abort", onAbort); - reader.releaseLock(); - } - - const bytes = Buffer.concat(chunks, totalBytes); + const bytes = await readBoundedStreamBytes( + response.body, + MAX_SKILL_COMPRESSED_BYTES, + new Error(`Compressed skill package exceeds the limit (${MAX_SKILL_COMPRESSED_BYTES} bytes).`), + requestSignal, + ); - if (hash.digest("hex") !== skill.blobSha256) { + if (createHash("sha256").update(bytes).digest("hex") !== skill.blobSha256) { throw new Error(`Skill blob checksum mismatch for ${skill.skillId}.`); } return bytes; diff --git a/tests/skill-materialization.test.ts b/tests/skill-materialization.test.ts index 857d868..fb43032 100644 --- a/tests/skill-materialization.test.ts +++ b/tests/skill-materialization.test.ts @@ -25,6 +25,7 @@ import type { DriverResolvedSkill } from "../src/protocol/boot"; import type { DriverExecutionInput } from "../src/protocol/execution"; import { exposeNativeSkillAliases } from "../src/runtimes/skill-bootstrap"; import { materializeResolvedSkills } from "../src/runtimes/skill-materialization"; +import { promiseWithTimeout } from "../src/utils/async"; import { bootPayload } from "./driver-runtime-boundary-fixtures"; const textEncoder = new TextEncoder(); @@ -936,7 +937,14 @@ Check the diff.`), } }); - test("cancels an oversized compressed stream before replacing the previous tree", async () => { + test.each([ + ["http-error", "cooperative"], + ["http-error", "stalled"], + ["content-length", "cooperative"], + ["content-length", "stalled"], + ["stream", "cooperative"], + ["stream", "stalled"], + ] as const)("bounds %s skill downloads with %s cancellation", async (boundary, cancellation) => { const root = await mkdtemp(join(tmpdir(), "mosoo-skill-materialization-")); const logger = createTestLogger(); const skill = createSkill(root, new Uint8Array(), { @@ -949,19 +957,34 @@ Check the diff.`), new ReadableStream({ cancel() { cancelled = true; + return cancellation === "stalled" ? new Promise(() => {}) : undefined; }, pull(controller) { - controller.enqueue(new Uint8Array(1024 * 1024)); + if (boundary === "stream") { + controller.enqueue(new Uint8Array(1024 * 1024)); + } }, }), + boundary === "http-error" + ? { status: 502 } + : boundary === "content-length" + ? { headers: { "content-length": "26214401" } } + : undefined, ); }) as unknown as typeof fetch; await mkdir(skill.mountPath, { recursive: true }); await writeFile(join(skill.mountPath, "SKILL.md"), "previous contents", "utf8"); try { - await expect(materialize(createExecution(root, skill), logger)).rejects.toThrow( - "Compressed skill package exceeds the limit", + await expect( + promiseWithTimeout(materialize(createExecution(root, skill), logger), { + label: "bounded skill download", + timeoutMs: 1_000, + }), + ).rejects.toThrow( + boundary === "http-error" + ? "Failed to download skill package" + : "Compressed skill package exceeds the limit", ); expect(cancelled).toBe(true); await expect(readFile(join(skill.mountPath, "SKILL.md"), "utf8")).resolves.toBe( From b3b0e34c4127f1e23fc941793665cb6ec763e4ee Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Sun, 30 Aug 2026 21:28:30 +0000 Subject: [PATCH 13/20] fix(openai): isolate transient app-server state --- README.md | 10 + src/protocol/paths/index.ts | 23 ++ src/protocol/start.ts | 2 + src/runtimes/openai/app-server-client.ts | 124 ++++-- src/runtimes/openai/auth-state.ts | 218 +++++++++- tests/openai-app-server-auth-state.test.ts | 444 ++++++++++++++++----- tests/openai-app-server-client.test.ts | 361 ++++++++++++++++- tests/openai-app-server-startup.test.ts | 50 ++- tests/public-api.test.ts | 39 +- 9 files changed, 1135 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index b405591..7ef66db 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,16 @@ The image contract in `environment-package-managers.json` exposes `npm` and `pip - public entries include typed exports - live artifact tests are gated by environment credentials +## OpenAI Credential Boundary + +Each OpenAI app-server process receives a private temporary `CODEX_HOME` that is deleted only after its supervised process tree stops. + +OpenAI persistence in the session home is limited to native rollout, memory, and SQLite state. + +It must never contain `auth.json` or be used as a credential archive. + +The Driver fails closed when it finds legacy credentials there and accepts OpenAI API-key auth only from the current execution environment. + ## Artifact Live Tests Every live test launches `dist/driver.mjs` as a child process and talks to it only through the production boot payload and control protocol. diff --git a/src/protocol/paths/index.ts b/src/protocol/paths/index.ts index 8efa807..08550b2 100644 --- a/src/protocol/paths/index.ts +++ b/src/protocol/paths/index.ts @@ -6,6 +6,7 @@ export const SANDBOX_SESSION_STATE_DIR = ".state"; export const SANDBOX_SESSION_ROOT = `${SANDBOX_WORKSPACE_ROOT}/se`; const SESSION_RESOURCE_MOUNT_DIR = "session-files"; +const SESSION_RESOURCE_BACKING_PREFIX = ".mosoo-session-files-"; export function getSessionWorkspacePath(sessionId: string): string { return `${SANDBOX_SESSION_ROOT}/${sessionId}`; @@ -23,6 +24,10 @@ export function getSessionResourceRootPath(sessionId: string): string { return `${getSessionWorkspacePath(sessionId)}/${SESSION_RESOURCE_MOUNT_DIR}`; } +export function getSessionResourceBackingPath(sessionId: string): string { + return `${getSessionWorkspacePath(sessionId)}/${SESSION_RESOURCE_BACKING_PREFIX}${sessionId}`; +} + export function getSessionRuntimeStatePath(sessionId: string, runtimeId: string): string { return `${getSessionStateRootPath(sessionId)}/${runtimeId}`; } @@ -59,6 +64,20 @@ export function isSandboxSessionStatePath(path: string): boolean { ); } +export function isSandboxSessionResourceBackingPath(path: string): boolean { + if (!isSandboxSessionPath(path)) { + return false; + } + + const [sessionId, backingSegment] = path.slice(SANDBOX_SESSION_ROOT.length + 1).split("/"); + + return ( + sessionId !== undefined && + sessionId.length > 0 && + backingSegment === `${SESSION_RESOURCE_BACKING_PREFIX}${sessionId}` + ); +} + function hasControlCharacter(value: string): boolean { for (const character of value) { const codePoint = character.codePointAt(0); @@ -144,6 +163,10 @@ export function normalizeSandboxFileBrowserPath(rawPath: string): string { throw new Error("Session runtime state is not visible in the Agent File Browser."); } + if (isSandboxSessionResourceBackingPath(path)) { + throw new Error("Session resource backing is not visible in the Agent File Browser."); + } + if (!isAllowedSandboxFileBrowserPath(path)) { throw new Error("Sandbox browser path is outside the Agent home."); } diff --git a/src/protocol/start.ts b/src/protocol/start.ts index 7016b37..80d9904 100644 --- a/src/protocol/start.ts +++ b/src/protocol/start.ts @@ -6,6 +6,7 @@ import type { DriverInstanceId } from "./id"; import type { DriverRuntime, DriverRuntimeTransport } from "./runtime"; export interface DriverStartInput { + readonly driverGeneration: number; readonly driverInstanceId: DriverInstanceId; readonly execution: DriverExecutionInput; readonly runtime: DriverRuntime; @@ -17,6 +18,7 @@ export function createDriverStartInputFromBootPayload( payload: DriverBootPayload, ): DriverStartInput { return { + driverGeneration: payload.driverGeneration, driverInstanceId: payload.driverInstanceId, execution: createDriverExecutionInputFromBootExecution(payload.execution), runtime: payload.runtime, diff --git a/src/runtimes/openai/app-server-client.ts b/src/runtimes/openai/app-server-client.ts index 7d1a050..448cd16 100644 --- a/src/runtimes/openai/app-server-client.ts +++ b/src/runtimes/openai/app-server-client.ts @@ -1,7 +1,6 @@ import { spawn } from "node:child_process"; import type { ChildProcessWithoutNullStreams } from "node:child_process"; import { once } from "node:events"; -import { mkdir } from "node:fs/promises"; import { createInterface } from "node:readline"; import type { Interface as ReadlineInterface } from "node:readline"; import { Transform } from "node:stream"; @@ -27,7 +26,9 @@ import { summarizeOpenAiProxyEnv } from "./app-server-env"; import { isRecord, readNonEmptyString, toJsonRpcId } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; import { - materializeOpenAiApiKeyAuthState, + cleanupOpenAiRuntimeHome, + createOpenAiRuntimeHome, + materializeOpenAiAuthState, materializeOpenAiModelProviderConfig, } from "./auth-state"; import type { @@ -69,6 +70,8 @@ interface OpenAiAppServerClientStartResult { readonly phases: readonly OpenAiAppServerClientStartPhase[]; } +type OpenAiRuntimeHomeState = Awaited>; + interface OpenAiClientContext extends AgentDriverContext { handleNotification( method: M, @@ -116,6 +119,7 @@ function summarizeJsonRpcErrorData(value: unknown): JsonObject | null { } const OPENAI_RUNTIME_HOME_ENV_NAME = "CODEX_HOME"; +const OPENAI_SQLITE_HOME_ENV_NAME = "CODEX_SQLITE_HOME"; const DEFAULT_OPENAI_RUNTIME_EXECUTABLE = "codex"; const APP_SERVER_TERMINATE_TIMEOUT_MS = 2_000; const APP_SERVER_KILL_TIMEOUT_MS = 1_000; @@ -208,6 +212,8 @@ export class OpenAiAppServerClient { #protocolFailureCommit: Promise | null = null; #protocolFailureTask: Promise | null = null; #readline: ReadlineInterface | null = null; + #runtimeHomeSetup: Promise | null = null; + #runtimeHomeState: OpenAiRuntimeHomeState | null = null; #serverMessageQueue: Promise = Promise.resolve(); #serverMessagesPaused = false; #startRequested = false; @@ -236,12 +242,28 @@ export class OpenAiAppServerClient { } this.#startRequested = true; + try { + return await this.#performStart(signal); + } catch (error) { + try { + await this.stop(); + } catch (cleanupError) { + const startupMessage = error instanceof Error ? error.message : "unknown error"; + throw new AggregateError( + [error, cleanupError], + `OpenAi app-server startup failed (${startupMessage}) and cleanup also failed.`, + ); + } + throw error; + } + } + + async #performStart(signal?: AbortSignal): Promise { const mcpConfig = buildOpenAiMcpServerConfig(this.#payload.execution.session.mcpServers); const env = buildRuntimeChildProcessEnv(this.#payload.execution.environment.paths, { ...process.env, ...this.#payload.execution.environment.variables, ...mcpConfig.env, - [OPENAI_RUNTIME_HOME_ENV_NAME]: this.#payload.execution.session.homePath, LOG_FORMAT: "json", }); const onAbort = () => { @@ -257,12 +279,17 @@ export class OpenAiAppServerClient { signal?.addEventListener("abort", onAbort, { once: true }); const phases: OpenAiAppServerClientStartPhase[] = []; - const measure = async (name: string, task: () => Promise): Promise => { + const measure = async ( + name: string, + task: () => Promise, + abortable = true, + ): Promise => { const startedAtMs = Date.now(); try { signal?.throwIfAborted(); - return await raceWithAbort(task(), signal); + const result = task(); + return await (abortable ? raceWithAbort(result, signal) : result); } finally { phases.push({ durationMs: toDurationMs(startedAtMs), @@ -270,25 +297,57 @@ export class OpenAiAppServerClient { }); } }; - const { homePath } = this.#payload.execution.session; - const runtimeHome = homePath; - await measure("app_server.home.mkdir", () => mkdir(runtimeHome, { recursive: true })); - - const authState = await measure("app_server.auth_state", () => - materializeOpenAiApiKeyAuthState({ - runtimeHome, - env, - }), - ); - const modelProviderConfig = await measure("app_server.config", () => - materializeOpenAiModelProviderConfig({ - env, - mcpServers: mcpConfig.mcpServers, - provider: this.#payload.execution.provider, - providerOptions: this.#payload.execution.providerOptions, - runtimeHome, - }), + const persistentRuntimeHome = this.#payload.execution.session.homePath; + const setupTask = (async () => { + const runtimeHomeState = await measure( + "app_server.home.create", + async () => { + const state = await createOpenAiRuntimeHome({ + driverGeneration: this.#payload.driverGeneration, + driverInstanceId: this.#payload.driverInstanceId, + persistentRuntimeHome, + ...(signal === undefined ? {} : { signal }), + }); + this.#runtimeHomeState = state; + return state; + }, + false, + ); + signal?.throwIfAborted(); + const { persistentRuntimeHome: sqliteHome, runtimeHome } = runtimeHomeState; + env[OPENAI_RUNTIME_HOME_ENV_NAME] = runtimeHome; + env[OPENAI_SQLITE_HOME_ENV_NAME] = sqliteHome; + const authState = await measure( + "app_server.auth_state", + () => materializeOpenAiAuthState({ env, runtimeHome }), + false, + ); + signal?.throwIfAborted(); + const modelProviderConfig = await measure( + "app_server.config", + () => + materializeOpenAiModelProviderConfig({ + env, + mcpServers: mcpConfig.mcpServers, + provider: this.#payload.execution.provider, + providerOptions: this.#payload.execution.providerOptions, + runtimeHome, + }), + false, + ); + return { authState, modelProviderConfig }; + })(); + const setupBarrier = setupTask.then( + () => undefined, + () => undefined, ); + this.#runtimeHomeSetup = setupBarrier; + void setupBarrier.finally(() => { + if (this.#runtimeHomeSetup === setupBarrier) { + this.#runtimeHomeSetup = null; + } + }); + const { authState, modelProviderConfig } = await raceWithAbort(setupTask, signal); this.#context.logger.debug("driver.openai.auth_state.prepared", { authJsonWritten: authState.written, @@ -636,6 +695,12 @@ export class OpenAiAppServerClient { } async #performStop(signal?: AbortSignal): Promise { + await this.#runtimeHomeSetup; + await this.#stopProcess(signal); + await this.#cleanupRuntimeHome(); + } + + async #stopProcess(signal?: AbortSignal): Promise { const child = this.#process; const target = this.#processTarget; const processClosed = this.#processClosed; @@ -712,6 +777,19 @@ export class OpenAiAppServerClient { } } + async #cleanupRuntimeHome(): Promise { + const state = this.#runtimeHomeState; + + if (state === null) { + return; + } + + await cleanupOpenAiRuntimeHome(state); + if (this.#runtimeHomeState === state) { + this.#runtimeHomeState = null; + } + } + #releaseProcessTreeMarker(marker: string): void { if (this.#processTreeMarker !== marker) { return; diff --git a/src/runtimes/openai/auth-state.ts b/src/runtimes/openai/auth-state.ts index 4f545d6..2925fbb 100644 --- a/src/runtimes/openai/auth-state.ts +++ b/src/runtimes/openai/auth-state.ts @@ -1,14 +1,40 @@ -import { join } from "node:path"; +import { chmod, lstat, mkdtemp, rename, rm, rmdir, symlink } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { isDriverId } from "../../protocol/id"; +import type { DriverInstanceId } from "../../protocol/id"; import type { JsonObject, JsonValue } from "../../protocol/json"; -import { writeFileAtomicallyAtPath } from "../atomic-file"; +import { + ensureAbsoluteRealDirectory, + ensureRealDirectoryAt, + hasErrorCode, + readPathStats, + writeFileAtomicallyAtPath, +} from "../atomic-file"; import { mergeProviderOptions } from "../provider-options"; -interface OpenAiApiKeyAuthStateInput { + +interface OpenAiRuntimeHomeInput { + driverGeneration: number; + driverInstanceId: DriverInstanceId; + persistentRuntimeHome: string; + signal?: AbortSignal; +} + +interface OpenAiRuntimeHomeState { + cleanupPath: string; + cleanupRoot: string | null; + readonly dev: number; + readonly ino: number; + readonly persistentRuntimeHome: string; + readonly runtimeHome: string; +} + +interface OpenAiAuthStateInput { env: NodeJS.ProcessEnv; runtimeHome: string; } -interface OpenAiApiKeyAuthStateResult { +interface OpenAiAuthStateResult { authJsonPath: string; hasApiKey: boolean; written: boolean; @@ -33,10 +59,18 @@ const OPENAI_COMPATIBLE_API_KEY_ENV_NAME = "OPENAI_COMPATIBLE_API_KEY"; const OPENAI_COMPATIBLE_BASE_URL_ENV_NAME = "OPENAI_COMPATIBLE_BASE_URL"; const OPENAI_BASE_URL_ENV_NAME = "OPENAI_BASE_URL"; const DISABLED_RUNTIME_FEATURES = ["plugins", "remote_plugin", "tool_suggest"] as const; +const OPENAI_RUNTIME_HOME_PREFIX = "/tmp/.mosoo-agent-driver-openai-"; +const OPENAI_RUNTIME_HOME_CLEANUP_PREFIX = "/tmp/.mosoo-agent-driver-openai-cleanup-"; +const PERSISTENT_OPENAI_RUNTIME_DIRECTORIES = [ + "sessions", + "archived_sessions", + "memories", + "memories_extensions", +] as const; function readOpenAiApiKey(env: NodeJS.ProcessEnv): string | null { const value = env["OPENAI_API_KEY"]?.trim(); - return value ?? null; + return value || null; } function readEnvVar(env: NodeJS.ProcessEnv, key: string): string | null { @@ -44,13 +78,179 @@ function readEnvVar(env: NodeJS.ProcessEnv, key: string): string | null { return value || null; } -export async function materializeOpenAiApiKeyAuthState( - input: OpenAiApiKeyAuthStateInput, -): Promise { +function assertOpenAiRuntimeIdentity( + driverInstanceId: DriverInstanceId, + driverGeneration: number, +): void { + if ( + !isDriverId(driverInstanceId) || + !Number.isSafeInteger(driverGeneration) || + driverGeneration < 0 + ) { + throw new TypeError("OpenAI runtime identity is invalid."); + } +} + +async function readRuntimeHomeIdentity(runtimeHome: string): Promise<{ + readonly dev: number; + readonly ino: number; +} | null> { + try { + const stats = await lstat(runtimeHome); + return stats.isDirectory() && !stats.isSymbolicLink() + ? { dev: stats.dev, ino: stats.ino } + : null; + } catch (error) { + if (hasErrorCode(error, "ENOENT")) { + return null; + } + throw error; + } +} + +export async function createOpenAiRuntimeHome( + input: OpenAiRuntimeHomeInput, +): Promise { + input.signal?.throwIfAborted(); + assertOpenAiRuntimeIdentity(input.driverInstanceId, input.driverGeneration); + const persistentRuntimeHome = resolve(input.persistentRuntimeHome); + await using persistentHome = await ensureAbsoluteRealDirectory( + persistentRuntimeHome, + "Persistent OpenAI runtime home", + input.signal, + ); + const persistentAuthPath = join(persistentRuntimeHome, "auth.json"); + + if ((await readPathStats(persistentAuthPath)) !== null) { + throw new Error( + `Persistent OpenAI runtime home must not contain credentials: ${persistentAuthPath}.`, + ); + } + + for (const name of PERSISTENT_OPENAI_RUNTIME_DIRECTORIES) { + await using _directory = await ensureRealDirectoryAt( + persistentHome, + name, + `Persistent OpenAI ${name} directory`, + input.signal, + ); + } + + input.signal?.throwIfAborted(); + const runtimeHome = await mkdtemp( + `${OPENAI_RUNTIME_HOME_PREFIX}${input.driverInstanceId}-g${String(input.driverGeneration)}-`, + ); + + try { + input.signal?.throwIfAborted(); + await chmod(runtimeHome, 0o700); + await Promise.all( + PERSISTENT_OPENAI_RUNTIME_DIRECTORIES.map((name) => + symlink(join(persistentRuntimeHome, name), join(runtimeHome, name)), + ), + ); + input.signal?.throwIfAborted(); + const identity = await readRuntimeHomeIdentity(runtimeHome); + + if (identity === null) { + throw new Error(`OpenAI runtime home is not a real directory: ${runtimeHome}.`); + } + + return { + cleanupPath: runtimeHome, + cleanupRoot: null, + ...identity, + persistentRuntimeHome, + runtimeHome, + }; + } catch (error) { + try { + await rm(runtimeHome, { force: true, recursive: true }); + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + "OpenAI runtime home creation and cleanup failed.", + ); + } + throw error; + } +} + +export async function cleanupOpenAiRuntimeHome(state: OpenAiRuntimeHomeState): Promise { + let cleanupRoot = state.cleanupRoot; + + if (cleanupRoot === null) { + cleanupRoot = await mkdtemp(OPENAI_RUNTIME_HOME_CLEANUP_PREFIX); + const cleanupPath = join(cleanupRoot, "runtime-home"); + + try { + await rename(state.cleanupPath, cleanupPath); + } catch (error) { + let cleanupError: unknown = null; + try { + await rmdir(cleanupRoot); + } catch (failure) { + cleanupError = failure; + } + + if (hasErrorCode(error, "ENOENT") && cleanupError === null) { + return false; + } + throw cleanupError === null + ? error + : new AggregateError([error, cleanupError], "OpenAI cleanup quarantine failed."); + } + + state.cleanupPath = cleanupPath; + state.cleanupRoot = cleanupRoot; + } + + const identity = await readRuntimeHomeIdentity(state.cleanupPath); + + if (identity === null) { + if ((await readPathStats(state.cleanupPath)) !== null) { + throw new Error( + `OpenAI cleanup preserved an unexpected runtime home at ${state.cleanupPath}.`, + ); + } + await rmdir(cleanupRoot); + state.cleanupPath = state.runtimeHome; + state.cleanupRoot = null; + return true; + } + + if (identity.dev !== state.dev || identity.ino !== state.ino) { + throw new Error(`OpenAI cleanup preserved an unexpected runtime home at ${state.cleanupPath}.`); + } + + try { + await rm(state.cleanupPath, { recursive: true }); + await rmdir(cleanupRoot); + state.cleanupPath = state.runtimeHome; + state.cleanupRoot = null; + return true; + } catch (error) { + if (hasErrorCode(error, "ENOENT")) { + await rmdir(cleanupRoot).catch((cleanupError: unknown) => { + if (!hasErrorCode(cleanupError, "ENOENT")) { + throw cleanupError; + } + }); + state.cleanupPath = state.runtimeHome; + state.cleanupRoot = null; + return true; + } + throw error; + } +} + +export async function materializeOpenAiAuthState( + input: OpenAiAuthStateInput, +): Promise { const authJsonPath = join(input.runtimeHome, "auth.json"); const apiKey = readOpenAiApiKey(input.env); - if (!apiKey) { + if (apiKey === null) { return { authJsonPath, hasApiKey: false, diff --git a/tests/openai-app-server-auth-state.test.ts b/tests/openai-app-server-auth-state.test.ts index aa45b6d..27afa82 100644 --- a/tests/openai-app-server-auth-state.test.ts +++ b/tests/openai-app-server-auth-state.test.ts @@ -1,19 +1,60 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; +import { + lstat, + mkdir, + mkdtemp, + readFile, + readlink, + rename, + rm, + symlink, + writeFile, +} from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { createDriverId } from "../src/protocol/id"; +import type { DriverInstanceId } from "../src/protocol/id"; import { - materializeOpenAiApiKeyAuthState, + cleanupOpenAiRuntimeHome, + createOpenAiRuntimeHome, + materializeOpenAiAuthState, materializeOpenAiModelProviderConfig, } from "../src/runtimes/openai/auth-state"; -let runtimeHomes: string[] = []; +type RuntimeHomeState = Awaited>; + +const temporaryDirectories: string[] = []; +const runtimeHomeStates: RuntimeHomeState[] = []; +const PERSISTENT_DIRECTORIES = [ + "sessions", + "archived_sessions", + "memories", + "memories_extensions", +] as const; + +function createDriverInstanceId(): DriverInstanceId { + return createDriverId() as DriverInstanceId; +} -async function createRuntimeHome(): Promise { - const runtimeHome = await mkdtemp(join(tmpdir(), "mosoo-openai-auth-")); - runtimeHomes.push(runtimeHome); - return runtimeHome; +async function createTemporaryDirectory(prefix = "mosoo-openai-auth-"): Promise { + const directory = await mkdtemp(join(tmpdir(), prefix)); + temporaryDirectories.push(directory); + return directory; +} + +async function createRuntimeHome( + persistentRuntimeHome?: string, + driverGeneration = 0, +): Promise { + const persistentHome = persistentRuntimeHome ?? (await createTemporaryDirectory()); + const state = await createOpenAiRuntimeHome({ + driverGeneration, + driverInstanceId: createDriverInstanceId(), + persistentRuntimeHome: persistentHome, + }); + runtimeHomeStates.push(state); + return state; } function requireRecord(value: unknown, label: string): Record { @@ -36,58 +77,292 @@ function expectDisabledRuntimeFeatures(config: Record): void { }); } +async function runCrossProcessAuthChild( + authState: typeof import("../src/runtimes/openai/auth-state"), +): Promise { + const root = process.env["RACE_ROOT"]!; + const role = process.env["RACE_ROLE"]!; + const driverInstanceId = process.env["DRIVER_INSTANCE_ID"]! as DriverInstanceId; + let state: Awaited> | null = null; + const marker = (name: string) => `${root}/${name}`; + const waitFor = async (name: string) => { + const deadline = Date.now() + 5_000; + while (!(await Bun.file(marker(name)).exists())) { + if (Date.now() >= deadline) { + throw new Error(`Timed out waiting for cross-process auth marker ${name}.`); + } + await Bun.sleep(2); + } + }; + + try { + if (role === "old") { + state = await authState.createOpenAiRuntimeHome({ + driverGeneration: 1, + driverInstanceId, + persistentRuntimeHome: root, + }); + await Bun.write(marker("old-home.json"), JSON.stringify(state)); + await waitFor("successor-auth.json"); + await authState.materializeOpenAiAuthState({ + env: { OPENAI_API_KEY: "old-key" }, + runtimeHome: state.runtimeHome, + }); + await Bun.write(marker("old-auth.json"), JSON.stringify(state)); + await waitFor("release-old"); + await authState.cleanupOpenAiRuntimeHome(state); + state = null; + await Bun.write(marker("old-cleaned"), ""); + return; + } + + await waitFor("old-home.json"); + state = await authState.createOpenAiRuntimeHome({ + driverGeneration: 2, + driverInstanceId, + persistentRuntimeHome: root, + }); + await authState.materializeOpenAiAuthState({ + env: { OPENAI_API_KEY: "successor-key" }, + runtimeHome: state.runtimeHome, + }); + await Bun.write(marker("successor-auth.json"), JSON.stringify(state)); + await waitFor("old-auth.json"); + await Bun.write(marker("release-old"), ""); + await waitFor("old-cleaned"); + const auth = await Bun.file(`${state.runtimeHome}/auth.json`).json(); + await Bun.write(marker("successor-observed.json"), JSON.stringify(auth)); + await authState.cleanupOpenAiRuntimeHome(state); + state = null; + } finally { + if (state !== null) { + await authState.cleanupOpenAiRuntimeHome(state).catch(() => false); + } + } +} + +async function readChildError(child: ReturnType): Promise { + const [exitCode, stderr] = await Promise.all([ + child.exited, + child.stderr instanceof ReadableStream ? new Response(child.stderr).text() : "", + ]); + expect(exitCode, stderr).toBe(0); + return stderr; +} + afterEach(async () => { await Promise.all( - runtimeHomes.map((runtimeHome) => rm(runtimeHome, { force: true, recursive: true })), + runtimeHomeStates.splice(0).map((state) => cleanupOpenAiRuntimeHome(state).catch(() => false)), + ); + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { force: true, recursive: true })), ); - runtimeHomes = []; }); describe("OpenAI app-server auth state", () => { - test("atomically replaces API key auth symlinks with a private file", async () => { - const runtimeHome = await createRuntimeHome(); - const authJsonPath = join(runtimeHome, "auth.json"); - const symlinkTarget = join(runtimeHome, "auth-target.json"); - await writeFile(symlinkTarget, "untouched\n"); - await symlink(symlinkTarget, authJsonPath); - const input = { - env: { - OPENAI_API_KEY: "openai-key", - }, - runtimeHome, - }; + test("rejects runtime identities that cannot be safe path segments", async () => { + const persistentRuntimeHome = await createTemporaryDirectory(); - await expect(materializeOpenAiApiKeyAuthState(input)).resolves.toMatchObject({ - hasApiKey: true, - written: true, + await expect( + createOpenAiRuntimeHome({ + driverGeneration: -1, + driverInstanceId: createDriverInstanceId(), + persistentRuntimeHome, + }), + ).rejects.toThrow("identity is invalid"); + await expect( + createOpenAiRuntimeHome({ + driverGeneration: 0, + driverInstanceId: "../../credential" as DriverInstanceId, + persistentRuntimeHome, + }), + ).rejects.toThrow("identity is invalid"); + }); + + test("creates a private runtime home with only explicit persistent state links", async () => { + const state = await createRuntimeHome(); + + expect((await lstat(state.runtimeHome)).mode & 0o777).toBe(0o700); + for (const name of PERSISTENT_DIRECTORIES) { + const persistentPath = join(state.persistentRuntimeHome, name); + expect((await lstat(persistentPath)).isDirectory()).toBe(true); + expect((await lstat(join(state.runtimeHome, name))).isSymbolicLink()).toBe(true); + expect(await readlink(join(state.runtimeHome, name))).toBe(persistentPath); + } + }); + + test("persists rollout and memory state across isolated runtime-home generations", async () => { + const persistentRuntimeHome = await createTemporaryDirectory(); + const first = await createRuntimeHome(persistentRuntimeHome, 1); + + for (const name of PERSISTENT_DIRECTORIES) { + await writeFile(join(first.runtimeHome, name, "retained"), name); + } + await cleanupOpenAiRuntimeHome(first); + + const successor = await createRuntimeHome(persistentRuntimeHome, 2); + for (const name of PERSISTENT_DIRECTORIES) { + expect(await readFile(join(successor.runtimeHome, name, "retained"), "utf8")).toBe(name); + } + }); + + test("removes auth, config, and temporary home without following persistent links", async () => { + const state = await createRuntimeHome(); + await materializeOpenAiAuthState({ + env: { OPENAI_API_KEY: "openai-key" }, + runtimeHome: state.runtimeHome, + }); + await materializeOpenAiModelProviderConfig({ + env: { OPENAI_API_KEY: "openai-key" }, + provider: "openai", + runtimeHome: state.runtimeHome, + }); + await writeFile(join(state.runtimeHome, "sessions", "retained"), "session"); + + expect(await cleanupOpenAiRuntimeHome(state)).toBe(true); + expect(await cleanupOpenAiRuntimeHome(state)).toBe(false); + await expect(lstat(state.runtimeHome)).rejects.toThrow(); + expect(await readFile(join(state.persistentRuntimeHome, "sessions", "retained"), "utf8")).toBe( + "session", + ); + }); + + test("preserves a real directory that replaces the owned runtime-home path", async () => { + const state = await createRuntimeHome(); + const movedOwnedHome = `${state.runtimeHome}.moved`; + + try { + await materializeOpenAiAuthState({ + env: { OPENAI_API_KEY: "owned-key" }, + runtimeHome: state.runtimeHome, + }); + await rename(state.runtimeHome, movedOwnedHome); + await mkdir(state.runtimeHome); + await writeFile(join(state.runtimeHome, "replacement-marker"), "preserve"); + + await expect(cleanupOpenAiRuntimeHome(state)).rejects.toThrow( + "preserved an unexpected runtime home", + ); + expect(state.cleanupRoot).not.toBeNull(); + expect(await readFile(join(state.cleanupPath, "replacement-marker"), "utf8")).toBe( + "preserve", + ); + expect(await readFile(join(movedOwnedHome, "auth.json"), "utf8")).toContain("owned-key"); + } finally { + if (state.cleanupRoot !== null) { + await rm(state.cleanupRoot, { force: true, recursive: true }).catch(() => {}); + } + state.cleanupPath = state.runtimeHome; + state.cleanupRoot = null; + await rm(state.runtimeHome, { force: true, recursive: true }); + await rm(movedOwnedHome, { force: true, recursive: true }); + } + }); + + test("isolates late predecessor materialization and cleanup across child processes", async () => { + const root = await createTemporaryDirectory("mosoo-openai-process-race-"); + const authStateModule = join(import.meta.dir, "../src/runtimes/openai/auth-state.ts"); + const source = `import * as authState from ${JSON.stringify(authStateModule)}; +await (${runCrossProcessAuthChild.toString()})(authState)`; + const commonEnv = Object.fromEntries( + Object.entries({ + ...process.env, + DRIVER_INSTANCE_ID: createDriverInstanceId(), + RACE_ROOT: root, + }).filter((entry): entry is [string, string] => entry[1] !== undefined), + ); + const old = Bun.spawn([process.execPath, "-e", source], { + env: { ...commonEnv, RACE_ROLE: "old" }, + stderr: "pipe", + stdout: "ignore", + }); + const successor = Bun.spawn([process.execPath, "-e", source], { + env: { ...commonEnv, RACE_ROLE: "successor" }, + stderr: "pipe", + stdout: "ignore", + }); + + try { + await Promise.all([readChildError(old), readChildError(successor)]); + expect( + JSON.parse(await readFile(join(root, "successor-observed.json"), "utf8")), + ).toMatchObject({ + OPENAI_API_KEY: "successor-key", + auth_mode: "apikey", + }); + const oldState = JSON.parse(await readFile(join(root, "old-home.json"), "utf8")) as { + runtimeHome: string; + }; + const successorState = JSON.parse( + await readFile(join(root, "successor-auth.json"), "utf8"), + ) as { runtimeHome: string }; + await expect(lstat(oldState.runtimeHome)).rejects.toThrow(); + await expect(lstat(successorState.runtimeHome)).rejects.toThrow(); + } finally { + old.kill("SIGKILL"); + successor.kill("SIGKILL"); + await Promise.allSettled([old.exited, successor.exited]); + } + }); + + test("writes API-key auth as a private regular file", async () => { + const { runtimeHome } = await createRuntimeHome(); + const result = await materializeOpenAiAuthState({ + env: { OPENAI_API_KEY: "openai-key" }, + runtimeHome, }); - const authStat = await lstat(authJsonPath); - expect(authStat.isSymbolicLink()).toBe(false); - expect(authStat.mode & 0o777).toBe(0o600); - expect(JSON.parse(await readFile(authJsonPath, "utf8"))).toMatchObject({ + + expect(result).toMatchObject({ hasApiKey: true, written: true }); + expect((await lstat(result.authJsonPath)).isFile()).toBe(true); + expect((await lstat(result.authJsonPath)).mode & 0o777).toBe(0o600); + expect(JSON.parse(await readFile(result.authJsonPath, "utf8"))).toMatchObject({ OPENAI_API_KEY: "openai-key", auth_mode: "apikey", }); - expect(await readFile(symlinkTarget, "utf8")).toBe("untouched\n"); - expect((await readdir(runtimeHome)).filter((name) => name.endsWith(".tmp"))).toEqual([]); }); - test("removes the private temporary file when auth replacement fails", async () => { - const runtimeHome = await createRuntimeHome(); - await mkdir(join(runtimeHome, "auth.json")); + test("does not create auth state without an injected API key", async () => { + const { runtimeHome } = await createRuntimeHome(); + const result = await materializeOpenAiAuthState({ env: {}, runtimeHome }); - await expect( - materializeOpenAiApiKeyAuthState({ - env: { OPENAI_API_KEY: "openai-key" }, - runtimeHome, - }), - ).rejects.toThrow(); - expect(await readdir(runtimeHome)).toEqual(["auth.json"]); + expect(result).toMatchObject({ hasApiKey: false, written: false }); + await expect(lstat(result.authJsonPath)).rejects.toThrow(); }); - test("writes model provider config for OpenAI-compatible credentials", async () => { - const runtimeHome = await createRuntimeHome(); + test("fails closed without modifying legacy workspace credentials", async () => { + for (const kind of ["regular", "external-link"] as const) { + const persistentRuntimeHome = await createTemporaryDirectory(); + const authJsonPath = join(persistentRuntimeHome, "auth.json"); + const externalTarget = join(persistentRuntimeHome, "external-auth.json"); + + if (kind === "regular") { + await writeFile(authJsonPath, "oauth-secret\n"); + } else { + await writeFile(externalTarget, "external-oauth-secret\n"); + await symlink(externalTarget, authJsonPath); + } + + await expect( + createOpenAiRuntimeHome({ + driverGeneration: 0, + driverInstanceId: createDriverInstanceId(), + persistentRuntimeHome, + }), + ).rejects.toThrow("must not contain credentials"); + + if (kind === "regular") { + expect(await readFile(authJsonPath, "utf8")).toBe("oauth-secret\n"); + } else { + expect(await readlink(authJsonPath)).toBe(externalTarget); + expect(await readFile(externalTarget, "utf8")).toBe("external-oauth-secret\n"); + } + } + }); + test("writes model provider config for OpenAI-compatible credentials", async () => { + const runtimeHome = await createTemporaryDirectory(); const result = await materializeOpenAiModelProviderConfig({ env: { OPENAI_COMPATIBLE_API_KEY: "compat-key", @@ -95,23 +370,16 @@ describe("OpenAI app-server auth state", () => { }, provider: "openai-compatible", providerOptions: { - features: { - tool_suggest: true, - }, - model_providers: { - "openai-compatible": { - wire_api: "chat", - }, - }, + features: { tool_suggest: true }, + model_providers: { "openai-compatible": { wire_api: "chat" } }, sandbox_workspace_write: true, }, runtimeHome, }); - - expect(result.written).toBe(true); const config = await readGeneratedConfig(result.configTomlPath); const modelProviders = requireRecord(config["model_providers"], "model providers"); + expect(result.written).toBe(true); expect(config["model_provider"]).toBe("openai-compatible"); expect(modelProviders["openai-compatible"]).toEqual({ base_url: "https://compat.example/v1", @@ -128,8 +396,7 @@ describe("OpenAI app-server auth state", () => { }); test("writes generated config for built-in OpenAI auth", async () => { - const runtimeHome = await createRuntimeHome(); - + const runtimeHome = await createTemporaryDirectory(); const result = await materializeOpenAiModelProviderConfig({ env: { OPENAI_API_KEY: "openai-key", @@ -138,10 +405,9 @@ describe("OpenAI app-server auth state", () => { provider: "openai", runtimeHome, }); - - expect(result.written).toBe(true); const config = await readGeneratedConfig(result.configTomlPath); + expect(result.written).toBe(true); expect(config["model_provider"]).toBeUndefined(); expect(config["model_providers"]).toBeUndefined(); expect(config["openai_base_url"]).toBe("https://proxy.example/v1"); @@ -149,12 +415,9 @@ describe("OpenAI app-server auth state", () => { }); test("passes reasoning effort and verbosity provider options into generated config", async () => { - const runtimeHome = await createRuntimeHome(); - + const runtimeHome = await createTemporaryDirectory(); const result = await materializeOpenAiModelProviderConfig({ - env: { - OPENAI_API_KEY: "openai-key", - }, + env: { OPENAI_API_KEY: "openai-key" }, provider: "openai", providerOptions: { model_reasoning_effort: "high", @@ -162,66 +425,51 @@ describe("OpenAI app-server auth state", () => { }, runtimeHome, }); - - expect(result.written).toBe(true); const config = await readGeneratedConfig(result.configTomlPath); + expect(result.written).toBe(true); expect(config["model_reasoning_effort"]).toBe("high"); expect(config["model_verbosity"]).toBe("low"); expectDisabledRuntimeFeatures(config); }); - test("writes mcp_servers tables into the generated config", async () => { - const runtimeHome = await createRuntimeHome(); - - const result = await materializeOpenAiModelProviderConfig({ - env: { - OPENAI_API_KEY: "openai-key", - }, + test("writes only configured mcp_servers tables", async () => { + const runtimeHome = await createTemporaryDirectory(); + const input = { + env: { OPENAI_API_KEY: "openai-key" }, + provider: "openai", + runtimeHome, + }; + const withServer = await materializeOpenAiModelProviderConfig({ + ...input, mcpServers: { Linear: { bearer_token_env_var: "MOSOO_MCP_BEARER_TOKEN_0", url: "https://api.example/driver/mcp/proxy/server-1", }, }, - provider: "openai", - runtimeHome, }); + const config = await readGeneratedConfig(withServer.configTomlPath); - expect(result.written).toBe(true); - const config = await readGeneratedConfig(result.configTomlPath); - const mcpServers = requireRecord(config["mcp_servers"], "mcp servers"); - - expect(mcpServers["Linear"]).toEqual({ + expect(requireRecord(config["mcp_servers"], "mcp servers")["Linear"]).toEqual({ bearer_token_env_var: "MOSOO_MCP_BEARER_TOKEN_0", url: "https://api.example/driver/mcp/proxy/server-1", }); - expectDisabledRuntimeFeatures(config); - }); - test("omits mcp_servers when no servers are wired", async () => { - const runtimeHome = await createRuntimeHome(); - - const result = await materializeOpenAiModelProviderConfig({ - env: { - OPENAI_API_KEY: "openai-key", - }, + const withoutServers = await materializeOpenAiModelProviderConfig({ + ...input, mcpServers: {}, - provider: "openai", - runtimeHome, }); - - const config = await readGeneratedConfig(result.configTomlPath); - expect(config["mcp_servers"]).toBeUndefined(); + expect( + (await readGeneratedConfig(withoutServers.configTomlPath))["mcp_servers"], + ).toBeUndefined(); }); test("skips unchanged regular config writes but replaces an unchanged symlink", async () => { - const runtimeHome = await createRuntimeHome(); + const runtimeHome = await createTemporaryDirectory(); const configTomlPath = join(runtimeHome, "config.toml"); const input = { - env: { - OPENAI_API_KEY: "openai-key", - }, + env: { OPENAI_API_KEY: "openai-key" }, provider: "openai", runtimeHome, }; @@ -247,13 +495,11 @@ describe("OpenAI app-server auth state", () => { }); test("fails OpenAI-compatible provider config when credentials are incomplete", async () => { - const runtimeHome = await createRuntimeHome(); + const runtimeHome = await createTemporaryDirectory(); await expect( materializeOpenAiModelProviderConfig({ - env: { - OPENAI_COMPATIBLE_API_KEY: "compat-key", - }, + env: { OPENAI_COMPATIBLE_API_KEY: "compat-key" }, provider: "openai-compatible", runtimeHome, }), diff --git a/tests/openai-app-server-client.test.ts b/tests/openai-app-server-client.test.ts index a8c5f6f..d4149f1 100644 --- a/tests/openai-app-server-client.test.ts +++ b/tests/openai-app-server-client.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, spyOn, test } from "bun:test"; import type { ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; import { readFileSync } from "node:fs"; -import { chmod, mkdtemp, rm } from "node:fs/promises"; +import { chmod, lstat, mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Readable } from "node:stream"; @@ -15,6 +15,7 @@ import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { PermissionEventDeliveryError } from "../src/core/driver-permission-broker"; import * as childProcessHelpers from "../src/runtimes/child-process"; import { OpenAiAppServerClient, limitNdjsonLines } from "../src/runtimes/openai/app-server-client"; +import * as openAiAuthState from "../src/runtimes/openai/auth-state"; import type { ServerNotificationMethod } from "../src/runtimes/openai/app-server-protocol"; import { settlePromiseWithTimeout } from "../src/utils/async"; import { driverBootPayload } from "./driver-boot-payload-fixture"; @@ -28,6 +29,55 @@ const initializeResult = { userAgent: "test-app-server/0.151.0", } as const; const initializeResultJson = JSON.stringify(initializeResult); +const nativeResumeResultJson = JSON.stringify({ + activePermissionProfile: null, + approvalPolicy: "on-request", + approvalsReviewer: "user", + cwd: "/workspace", + instructionSources: [], + model: "gpt-5.6", + modelProvider: "openai", + multiAgentMode: "explicitRequestOnly", + reasoningEffort: "high", + runtimeWorkspaceRoots: ["/workspace"], + sandbox: { + excludeSlashTmp: false, + excludeTmpdirEnvVar: false, + networkAccess: false, + type: "workspaceWrite", + writableRoots: ["/workspace"], + }, + serviceTier: null, + thread: { + agentNickname: null, + agentRole: null, + canAcceptDirectInput: true, + cliVersion: "0.151.0", + createdAt: 1, + cwd: "/workspace", + ephemeral: false, + extra: null, + forkedFromId: null, + gitInfo: null, + historyMode: "paginated", + id: "thread-1", + modelProvider: "openai", + name: null, + parentThreadId: null, + path: null, + preview: "retained", + projectId: null, + recencyAt: null, + section: null, + sectionEnteredAt: null, + sessionId: "thread-1", + source: "appServer", + status: { type: "idle" }, + threadSource: null, + turns: [], + updatedAt: 2, + }, +}); function isProcessRunning(pid: number): boolean { try { @@ -57,6 +107,7 @@ async function createClientHarness( requestPermission: AgentDriverPermissionPort["request"] = async () => "allow_once", interpreter = "bun", environment: DriverExecutionEnvironment = driverBootPayload.execution.environment, + runtimeOptions: { driverGeneration?: number; homePath?: string } = {}, ) { const directory = await mkdtemp(join(tmpdir(), "mosoo-openai-client-")); temporaryDirectories.push(directory); @@ -67,6 +118,7 @@ async function createClientHarness( const payload = createDriverStartInputFromBootPayload({ ...driverBootPayload, + driverGeneration: runtimeOptions.driverGeneration ?? driverBootPayload.driverGeneration, execution: { ...driverBootPayload.execution, environment, @@ -74,7 +126,7 @@ async function createClientHarness( ...driverBootPayload.execution.session, context: { ...driverBootPayload.execution.session.context, - homePath: join(directory, "home"), + homePath: runtimeOptions.homePath ?? join(directory, "home"), sessionOrganizationPath: directory, }, cwd: directory, @@ -120,6 +172,196 @@ afterEach(async () => { }); describe("OpenAi app-server client", () => { + test("keeps transient API-key auth for the fake app-server lifetime", async () => { + const harness = await createClientHarness( + (directory) => ` +await Bun.write(${JSON.stringify(join(directory, "runtime-env.json"))}, JSON.stringify({ + codexHome: process.env.CODEX_HOME, + sqliteHome: process.env.CODEX_SQLITE_HOME, +})); +await Bun.write(process.env.CODEX_SQLITE_HOME + "/state.sqlite", "sqlite-state"); +let buffer = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { + buffer += chunk; + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const request = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (request.method === "initialize") { + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); + } + } +}); +setInterval(() => {}, 1000); +`, + undefined, + undefined, + "bun", + { + ...driverBootPayload.execution.environment, + variables: { OPENAI_API_KEY: "client-lifetime-key" }, + }, + ); + + try { + await harness.client.start(); + const runtimeEnv = JSON.parse( + await readFile(join(harness.directory, "runtime-env.json"), "utf8"), + ) as { codexHome: string; sqliteHome: string }; + const authJsonPath = join(runtimeEnv.codexHome, "auth.json"); + + expect(runtimeEnv.codexHome).not.toBe(join(harness.directory, "home")); + expect(runtimeEnv.sqliteHome).toBe(join(harness.directory, "home")); + expect((await lstat(authJsonPath)).isFile()).toBe(true); + expect((await lstat(authJsonPath)).mode & 0o777).toBe(0o600); + expect(JSON.parse(await readFile(authJsonPath, "utf8"))).toMatchObject({ + OPENAI_API_KEY: "client-lifetime-key", + }); + await expect(harness.client.start()).rejects.toThrow("cannot be started more than once"); + expect(await readFile(authJsonPath, "utf8")).toContain("client-lifetime-key"); + await harness.client.stop(); + await expect(lstat(runtimeEnv.codexHome)).rejects.toThrow(); + expect((await lstat(join(runtimeEnv.sqliteHome, "sessions"))).isDirectory()).toBe(true); + expect(await readFile(join(runtimeEnv.sqliteHome, "state.sqlite"), "utf8")).toBe( + "sqlite-state", + ); + await expect(lstat(join(runtimeEnv.sqliteHome, "auth.json"))).rejects.toThrow(); + } finally { + await harness.client.stop().catch(() => {}); + } + }); + + test("cleans transient API-key auth when fake app-server startup fails", async () => { + const harness = await createClientHarness( + (directory) => ` +await Bun.write(${JSON.stringify(join(directory, "failed-runtime-home"))}, process.env.CODEX_HOME); +process.stdin.once("data", (chunk) => { + const request = JSON.parse(String(chunk).trim()); + process.stdout.write(JSON.stringify({ + error: { code: -32000, message: "initialize rejected" }, + id: request.id, + }) + "\\n"); +}); +setInterval(() => {}, 1000); +`, + undefined, + undefined, + "bun", + { + ...driverBootPayload.execution.environment, + variables: { OPENAI_API_KEY: "failed-start-key" }, + }, + ); + + try { + await expect(harness.client.start()).rejects.toThrow("initialize rejected"); + const runtimeHome = await readFile(join(harness.directory, "failed-runtime-home"), "utf8"); + await expect(lstat(runtimeHome)).rejects.toThrow(); + await expect(lstat(join(harness.directory, "home", "auth.json"))).rejects.toThrow(); + } finally { + await harness.client.stop().catch(() => {}); + } + }); + + test("lists and resumes persistent native state across Driver generations", async () => { + const sharedRoot = await mkdtemp(join(tmpdir(), "mosoo-openai-native-state-")); + temporaryDirectories.push(sharedRoot); + const persistentRuntimeHome = join(sharedRoot, "home"); + const environment = { + ...driverBootPayload.execution.environment, + variables: { OPENAI_API_KEY: "native-state-key" }, + }; + const first = await createClientHarness( + (directory) => ` +await Bun.write(process.env.CODEX_HOME + "/sessions/thread-1.jsonl", "rollout"); +await Bun.write(process.env.CODEX_SQLITE_HOME + "/state.sqlite", "sqlite"); +await Bun.write(${JSON.stringify(join(directory, "runtime-home"))}, process.env.CODEX_HOME); +let buffer = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { + buffer += chunk; + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const request = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (request.id !== undefined) { + process.stdout.write(JSON.stringify({ id: request.id, result: ${initializeResultJson} }) + "\\n"); + } + } +}); +setInterval(() => {}, 1000); +`, + undefined, + undefined, + "bun", + environment, + { driverGeneration: 1, homePath: persistentRuntimeHome }, + ); + + let firstRuntimeHome = ""; + try { + await first.client.start(); + firstRuntimeHome = await readFile(join(first.directory, "runtime-home"), "utf8"); + } finally { + await first.client.stop().catch(() => {}); + } + await expect(lstat(firstRuntimeHome)).rejects.toThrow(); + expect(await readFile(join(persistentRuntimeHome, "sessions", "thread-1.jsonl"), "utf8")).toBe( + "rollout", + ); + expect(await readFile(join(persistentRuntimeHome, "state.sqlite"), "utf8")).toBe("sqlite"); + + const successor = await createClientHarness( + (directory) => ` +import { readdirSync, readFileSync } from "node:fs"; +await Bun.write(${JSON.stringify(join(directory, "observation.json"))}, JSON.stringify({ + codexHome: process.env.CODEX_HOME, + sessions: readdirSync(process.env.CODEX_HOME + "/sessions"), + sqlite: readFileSync(process.env.CODEX_SQLITE_HOME + "/state.sqlite", "utf8"), +})); +let buffer = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { + buffer += chunk; + let newline; + while ((newline = buffer.indexOf("\\n")) >= 0) { + const request = JSON.parse(buffer.slice(0, newline)); + buffer = buffer.slice(newline + 1); + if (request.id === undefined) continue; + const result = request.method === "thread/resume" + ? ${nativeResumeResultJson} + : ${initializeResultJson}; + process.stdout.write(JSON.stringify({ id: request.id, result }) + "\\n"); + } +}); +setInterval(() => {}, 1000); +`, + undefined, + undefined, + "bun", + environment, + { driverGeneration: 2, homePath: persistentRuntimeHome }, + ); + + try { + await successor.client.start(); + const observation = JSON.parse( + await readFile(join(successor.directory, "observation.json"), "utf8"), + ) as { codexHome: string; sessions: string[]; sqlite: string }; + expect(observation.codexHome).not.toBe(firstRuntimeHome); + expect(observation.sessions).toContain("thread-1.jsonl"); + expect(observation.sqlite).toBe("sqlite"); + await expect( + successor.client.request("thread/resume", { threadId: "thread-1" }), + ).resolves.toMatchObject({ thread: { id: "thread-1" } }); + await successor.client.stop(); + await expect(lstat(observation.codexHome)).rejects.toThrow(); + } finally { + await successor.client.stop().catch(() => {}); + } + }); + test("declares only initialized server-request capabilities it handles", async () => { const harness = await createClientHarness( (directory) => ` @@ -629,6 +871,109 @@ setInterval(() => {}, 1000); } }); + test.each(["home", "auth"] as const)( + "waits for in-flight %s setup before cleaning the private runtime home", + async (phase) => { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + const nativeCreate = openAiAuthState.createOpenAiRuntimeHome; + const nativeAuth = openAiAuthState.materializeOpenAiAuthState; + const createSpy = spyOn(openAiAuthState, "createOpenAiRuntimeHome"); + const authSpy = spyOn(openAiAuthState, "materializeOpenAiAuthState"); + + if (phase === "home") { + createSpy.mockImplementation(async (input) => { + const state = await nativeCreate(input); + entered.resolve(state.runtimeHome); + await release.promise; + return state; + }); + } else { + authSpy.mockImplementation(async (input) => { + entered.resolve(input.runtimeHome); + await release.promise; + return nativeAuth(input); + }); + } + + const harness = await createClientHarness( + (directory) => ` +await Bun.write(${JSON.stringify(join(directory, "setup-race-spawned"))}, "spawned"); +setInterval(() => {}, 1000); +`, + undefined, + undefined, + "bun", + { + ...driverBootPayload.execution.environment, + variables: { OPENAI_API_KEY: "setup-race-key" }, + }, + ); + + try { + const start = harness.client.start(); + void start.catch(() => {}); + const runtimeHome = await entered.promise; + const stop = harness.client.stop(); + + await expect( + settlePromiseWithTimeout(stop, { + label: "OpenAI setup-barrier stop", + timeoutMs: 25, + }), + ).resolves.toMatchObject({ status: "timed_out" }); + expect((await lstat(runtimeHome)).isDirectory()).toBe(true); + + release.resolve(); + await expect(stop).resolves.toBeUndefined(); + await expect(start).rejects.toThrow("stopped during startup"); + await expect(lstat(runtimeHome)).rejects.toThrow(); + expect(await Bun.file(join(harness.directory, "setup-race-spawned")).exists()).toBe(false); + } finally { + release.resolve(); + createSpy.mockRestore(); + authSpy.mockRestore(); + await harness.client.stop().catch(() => {}); + } + }, + ); + + test("cleans its private runtime home when startup is aborted", async () => { + const harness = await createClientHarness( + (directory) => ` +await Bun.write(${JSON.stringify(join(directory, "aborted-runtime-home"))}, process.env.CODEX_HOME); +process.stdin.resume(); +setInterval(() => {}, 1000); +`, + undefined, + undefined, + "bun", + { + ...driverBootPayload.execution.environment, + variables: { OPENAI_API_KEY: "aborted-start-key" }, + }, + ); + const controller = new AbortController(); + + try { + const start = harness.client.start(controller.signal); + void start.catch(() => {}); + const runtimeHomeFile = Bun.file(join(harness.directory, "aborted-runtime-home")); + for (let attempt = 0; attempt < 100 && !(await runtimeHomeFile.exists()); attempt += 1) { + await Bun.sleep(5); + } + const runtimeHome = await runtimeHomeFile.text(); + controller.abort(new Error("startup aborted")); + + await expect(start).rejects.toThrow("startup aborted"); + await expect(lstat(runtimeHome)).rejects.toThrow(); + await expect(lstat(join(harness.directory, "home", "auth.json"))).rejects.toThrow(); + } finally { + controller.abort(); + await harness.client.stop().catch(() => {}); + } + }); + test("admits only one concurrent start", async () => { const harness = await createClientHarness( (directory) => ` @@ -1513,6 +1858,7 @@ setInterval(() => {}, 1000); const harness = await createClientHarness( (directory) => ` import { spawn } from "node:child_process"; +await Bun.write(${JSON.stringify(join(directory, "runtime-home"))}, process.env.CODEX_HOME); const descendant = spawn("/usr/bin/setsid", [ process.execPath, "-e", @@ -1534,6 +1880,13 @@ process.stdin.on("data", (chunk) => { process.on("SIGTERM", () => process.exit(0)); setInterval(() => {}, 1000); `, + undefined, + undefined, + "bun", + { + ...driverBootPayload.execution.environment, + variables: { OPENAI_API_KEY: "retry-stop-key" }, + }, ); const nativeKill = process.kill; let childPid = 0; @@ -1542,6 +1895,7 @@ setInterval(() => {}, 1000); try { await harness.client.start(); + const runtimeHome = await Bun.file(join(harness.directory, "runtime-home")).text(); childPid = Number(await Bun.file(join(harness.directory, "pid")).text()); descendantPid = Number(await Bun.file(join(harness.directory, "descendant-pid")).text()); process.kill = ((pid, signal) => { @@ -1553,8 +1907,11 @@ setInterval(() => {}, 1000); }) as typeof process.kill; await expect(harness.client.stop()).rejects.toThrow("process tree did not exit"); + expect(await readFile(join(runtimeHome, "auth.json"), "utf8")).toContain("retry-stop-key"); suppressKill = false; await expect(harness.client.stop()).resolves.toBeUndefined(); + await expect(lstat(runtimeHome)).rejects.toThrow(); + expect((await lstat(join(harness.directory, "home", "sessions"))).isDirectory()).toBe(true); } finally { process.kill = nativeKill; diff --git a/tests/openai-app-server-startup.test.ts b/tests/openai-app-server-startup.test.ts index 3b783af..b2b3a84 100644 --- a/tests/openai-app-server-startup.test.ts +++ b/tests/openai-app-server-startup.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, spyOn, test } from "bun:test"; -import { chmod, mkdtemp, readFile, rm } from "node:fs/promises"; +import { chmod, lstat, mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -43,6 +43,7 @@ interface CancellationHarnessOptions { readonly duplicateRequestPhase?: "active_turn" | "turn_start"; readonly emitInterruptedTurnOnStart?: boolean; readonly emitToolCompletionOnTurnStart?: boolean; + readonly environmentVariables?: Readonly>; readonly failCancellationRequest?: boolean; readonly failInitialThreadStart?: boolean; readonly holdCancellationRequest?: boolean; @@ -114,7 +115,12 @@ const launchNumber = existsSync(${JSON.stringify(launchCountFile)}) ? Number(readFileSync(${JSON.stringify(launchCountFile)}, "utf8")) + 1 : 1; writeFileSync(${JSON.stringify(launchCountFile)}, String(launchNumber)); -appendFileSync(${JSON.stringify(processLog)}, JSON.stringify({ launchNumber, pid: process.pid }) + "\\n"); +appendFileSync(${JSON.stringify(processLog)}, JSON.stringify({ + codexHome: process.env.CODEX_HOME, + launchNumber, + pid: process.pid, + sqliteHome: process.env.CODEX_SQLITE_HOME, +}) + "\\n"); const sendInterrupted = (turnId) => process.stdout.write(JSON.stringify({ method: "turn/completed", params: { @@ -414,6 +420,13 @@ process.stdin.on("data", (chunk) => { ...driverBootPayload, execution: { ...driverBootPayload.execution, + environment: { + ...driverBootPayload.execution.environment, + variables: { + ...driverBootPayload.execution.environment.variables, + ...cancellationOptions.environmentVariables, + }, + }, permissionPolicy, session: { ...driverBootPayload.execution.session, @@ -606,6 +619,39 @@ function createCancellationHarness(options: CancellationHarnessOptions) { } describe("OpenAI app-server startup", () => { + test("keeps transient auth through fake app-server native-resume startup", async () => { + const harness = await createHarness( + "no rollout found for thread id stale-thread", + [], + false, + async () => "allow_once", + false, + "full_access", + false, + { environmentVariables: { OPENAI_API_KEY: "resume-startup-key" } }, + ); + try { + await harness.backend.start(harness.context, new AbortController().signal); + const firstLaunch = JSON.parse( + (await readFile(harness.processLog, "utf8")).trim().split("\n")[0] ?? "{}", + ) as { codexHome: string; sqliteHome: string }; + const authJsonPath = join(firstLaunch.codexHome, "auth.json"); + + expect(firstLaunch.sqliteHome).toBe(harness.payload.execution.session.homePath); + expect(JSON.parse(await readFile(authJsonPath, "utf8"))).toMatchObject({ + OPENAI_API_KEY: "resume-startup-key", + }); + expect(await readFile(harness.requestLog, "utf8")).toContain('"method":"thread/resume"'); + await harness.backend.stop(harness.context, "test complete", new AbortController().signal); + await expect(lstat(firstLaunch.codexHome)).rejects.toThrow(); + expect((await lstat(join(firstLaunch.sqliteHome, "sessions"))).isDirectory()).toBe(true); + } finally { + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + } + }); + test("maps supervised permissions to untrusted thread and turn policies", async () => { const harness = await createHarness( "no rollout found for thread id stale-thread", diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index 56754c6..991aa90 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -37,7 +37,13 @@ import { parseDriverReadyInput as parseDriverReadyInputFromOrpcSubpath, } from "@mosoo/agent-driver/orpc"; import type { DriverHeartbeatInput as DriverHeartbeatInputFromOrpcSubpath } from "@mosoo/agent-driver/orpc"; -import { SANDBOX_MEMORY_PATH as SANDBOX_MEMORY_PATH_FROM_PATHS_SUBPATH } from "@mosoo/agent-driver/paths"; +import { + SANDBOX_MEMORY_PATH as SANDBOX_MEMORY_PATH_FROM_PATHS_SUBPATH, + getSessionResourceBackingPath as getSessionResourceBackingPathFromSubpath, + getSessionResourceRootPath as getSessionResourceRootPathFromSubpath, + isSandboxSessionResourceBackingPath, + normalizeSandboxFileBrowserPath, +} from "@mosoo/agent-driver/paths"; import { filterOpenAiPrivateCitations } from "@mosoo/agent-driver/provider-output"; import { isSupportedDriverRuntime as isSupportedDriverRuntimeFromSubpath } from "@mosoo/agent-driver/runtime"; @@ -109,6 +115,37 @@ describe("public API", () => { }); expect(isSupportedDriverRuntimeFromSubpath("openai-runtime")).toBe(true); expect(SANDBOX_MEMORY_PATH_FROM_PATHS_SUBPATH).toBe("/workspace/memory"); + expect(getSessionResourceRootPathFromSubpath("session-1")).toBe( + "/workspace/se/session-1/session-files", + ); + expect(getSessionResourceBackingPathFromSubpath("session-1")).toBe( + "/workspace/se/session-1/.mosoo-session-files-session-1", + ); + expect( + isSandboxSessionResourceBackingPath( + "/workspace/se/session-1/.mosoo-session-files-session-1/nested.txt", + ), + ).toBe(true); + expect( + isSandboxSessionResourceBackingPath( + "/workspace/se/session-1/.mosoo-session-files-session-2/nested.txt", + ), + ).toBe(false); + expect( + isSandboxSessionResourceBackingPath( + "/workspace/se/session-1/public/.mosoo-session-files-session-1", + ), + ).toBe(false); + expect(() => + normalizeSandboxFileBrowserPath( + "/workspace/se/session-1/.mosoo-session-files-session-1/nested.txt", + ), + ).toThrow("Session resource backing is not visible"); + expect( + normalizeSandboxFileBrowserPath( + "/workspace/se/session-1/.mosoo-session-files-session-2/nested.txt", + ), + ).toBe("/workspace/se/session-1/.mosoo-session-files-session-2/nested.txt"); expect(filterOpenAiPrivateCitations("plain text")).toEqual({ privateCitationCount: 0, text: "plain text", From cc04212bf5b425c36191276e4ffa930ced56b313 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Sun, 30 Aug 2026 21:35:22 +0000 Subject: [PATCH 14/20] fix(storage): keep resource mounts outside snapshots --- src/protocol/paths/index.ts | 16 +++------------- tests/public-api.test.ts | 12 ++++-------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/protocol/paths/index.ts b/src/protocol/paths/index.ts index 08550b2..2932219 100644 --- a/src/protocol/paths/index.ts +++ b/src/protocol/paths/index.ts @@ -6,7 +6,7 @@ export const SANDBOX_SESSION_STATE_DIR = ".state"; export const SANDBOX_SESSION_ROOT = `${SANDBOX_WORKSPACE_ROOT}/se`; const SESSION_RESOURCE_MOUNT_DIR = "session-files"; -const SESSION_RESOURCE_BACKING_PREFIX = ".mosoo-session-files-"; +const SESSION_RESOURCE_BACKING_ROOT = `${SANDBOX_WORKSPACE_ROOT}/.mosoo/session-files`; export function getSessionWorkspacePath(sessionId: string): string { return `${SANDBOX_SESSION_ROOT}/${sessionId}`; @@ -25,7 +25,7 @@ export function getSessionResourceRootPath(sessionId: string): string { } export function getSessionResourceBackingPath(sessionId: string): string { - return `${getSessionWorkspacePath(sessionId)}/${SESSION_RESOURCE_BACKING_PREFIX}${sessionId}`; + return `${SESSION_RESOURCE_BACKING_ROOT}/${sessionId}`; } export function getSessionRuntimeStatePath(sessionId: string, runtimeId: string): string { @@ -65,17 +65,7 @@ export function isSandboxSessionStatePath(path: string): boolean { } export function isSandboxSessionResourceBackingPath(path: string): boolean { - if (!isSandboxSessionPath(path)) { - return false; - } - - const [sessionId, backingSegment] = path.slice(SANDBOX_SESSION_ROOT.length + 1).split("/"); - - return ( - sessionId !== undefined && - sessionId.length > 0 && - backingSegment === `${SESSION_RESOURCE_BACKING_PREFIX}${sessionId}` - ); + return hasConcreteChildPath(path, SESSION_RESOURCE_BACKING_ROOT); } function hasControlCharacter(value: string): boolean { diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index 991aa90..699befd 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -119,16 +119,14 @@ describe("public API", () => { "/workspace/se/session-1/session-files", ); expect(getSessionResourceBackingPathFromSubpath("session-1")).toBe( - "/workspace/se/session-1/.mosoo-session-files-session-1", + "/workspace/.mosoo/session-files/session-1", ); expect( - isSandboxSessionResourceBackingPath( - "/workspace/se/session-1/.mosoo-session-files-session-1/nested.txt", - ), + isSandboxSessionResourceBackingPath("/workspace/.mosoo/session-files/session-1/nested.txt"), ).toBe(true); expect( isSandboxSessionResourceBackingPath( - "/workspace/se/session-1/.mosoo-session-files-session-2/nested.txt", + "/workspace/.mosoo/session-files-other/session-1/nested.txt", ), ).toBe(false); expect( @@ -137,9 +135,7 @@ describe("public API", () => { ), ).toBe(false); expect(() => - normalizeSandboxFileBrowserPath( - "/workspace/se/session-1/.mosoo-session-files-session-1/nested.txt", - ), + normalizeSandboxFileBrowserPath("/workspace/.mosoo/session-files/session-1/nested.txt"), ).toThrow("Session resource backing is not visible"); expect( normalizeSandboxFileBrowserPath( From f5885e1e9003b9d6c9d6152842ab4c04650dbe8b Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Mon, 31 Aug 2026 05:06:09 +0000 Subject: [PATCH 15/20] fix(release): gate publication on live artifacts --- .github/workflows/release.yml | 15 ++++++++++++++ tests/driver-artifact-contract.test.ts | 28 +++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb5b184..55abe86 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -111,6 +111,21 @@ jobs: vp exec tsc --ignoreConfig --noEmit --moduleResolution Bundler --module ESNext --target ESNext "${declarations[@]}" bun test tests/driver-artifact-mcp.test.ts + - name: Test packed driver live matrix + timeout-minutes: 180 + env: + AGENT_DRIVER_LIVE_ARTIFACT: packed/dist/driver.mjs + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + run: | + set -euo pipefail + + if [[ -z "${OPENROUTER_API_KEY:-}" ]]; then + echo "::error::OPENROUTER_API_KEY is required for release live artifact tests." + exit 1 + fi + + vp run test:live:artifact + - id: image name: Build image env: diff --git a/tests/driver-artifact-contract.test.ts b/tests/driver-artifact-contract.test.ts index af82109..e72f5c9 100644 --- a/tests/driver-artifact-contract.test.ts +++ b/tests/driver-artifact-contract.test.ts @@ -37,8 +37,10 @@ interface EnvironmentPackageManagerManifest { } interface WorkflowStep { + readonly env?: Record; readonly name?: string; readonly run?: string; + readonly "timeout-minutes"?: number; readonly uses?: string; } @@ -235,6 +237,8 @@ describe("driver artifact contract", () => { expect(scripts["prepack"]).toBe("vp run build"); expect(containerfile).toContain("COPY dist/driver.mjs /usr/local/bin/agent-driver"); expect(scripts["test:live"]).toBe("vp run build && vp run test:live:artifact"); + expect(scripts["test:live:artifact"]).toContain("AGENT_DRIVER_LIVE_SUITE=all"); + expect(scripts["test:live:artifact"]).toContain("tests/driver-artifact-live.test.ts"); expect(scripts["test:live:artifact"]).toContain("tests/driver-artifact-mcp.test.ts"); for (const suite of ["anthropic", "openai", "opencode"] as const) { @@ -246,7 +250,6 @@ describe("driver artifact contract", () => { test("keeps release publication ordered, monotonic, and verifiable", () => { const prWorkflow = readWorkflow("../.github/workflows/pr.yml"); - const releaseWorkflowText = readText("../.github/workflows/release.yml"); const releaseWorkflow = readWorkflow("../.github/workflows/release.yml"); const verifyJob = workflowJob(releaseWorkflow, "verify"); const buildJob = workflowJob(releaseWorkflow, "build"); @@ -254,6 +257,10 @@ describe("driver artifact contract", () => { const npmJob = workflowJob(releaseWorkflow, "publish-npm"); const latestJob = workflowJob(releaseWorkflow, "publish-latest-image"); const verifyRun = workflowStep(verifyJob, "Verify release tag").run ?? ""; + const packStep = workflowStep(buildJob, "Pack package"); + const mcpStep = workflowStep(buildJob, "Test packed driver"); + const liveStep = workflowStep(buildJob, "Test packed driver live matrix"); + const imageBuildStep = workflowStep(buildJob, "Build image"); const imageRun = workflowStep(imageJob, "Publish versioned image").run ?? ""; const npmRun = workflowStep(npmJob, "Publish package").run ?? ""; const buildRun = @@ -312,8 +319,23 @@ describe("driver artifact contract", () => { 'skopeo copy --preserve-digests "docker://$IMAGE@$DIGEST" "docker://$IMAGE:latest"', ]) expect(latestRun).toContain(marker); - expect(releaseWorkflowText).not.toContain("OPENROUTER_API_KEY"); - expect(releaseWorkflowText).not.toContain("test:live:artifact"); + expect(packStep.env).toBeUndefined(); + expect(mcpStep.env).not.toHaveProperty("OPENROUTER_API_KEY"); + expect(mcpStep.run).toContain("bun test tests/driver-artifact-mcp.test.ts"); + expect(liveStep).toMatchObject({ + env: { + AGENT_DRIVER_LIVE_ARTIFACT: "packed/dist/driver.mjs", + OPENROUTER_API_KEY: "${{ secrets.OPENROUTER_API_KEY }}", + }, + "timeout-minutes": 180, + }); + expect(liveStep.run).toContain('if [[ -z "${OPENROUTER_API_KEY:-}" ]]'); + expect(liveStep.run).toContain("vp run test:live:artifact"); + const buildSteps = buildJob.steps ?? []; + expect(buildSteps.indexOf(packStep)).toBeLessThan(buildSteps.indexOf(mcpStep)); + expect(buildSteps.indexOf(mcpStep)).toBeLessThan(buildSteps.indexOf(liveStep)); + expect(buildSteps.indexOf(liveStep)).toBeLessThan(buildSteps.indexOf(imageBuildStep)); + expect(readText("../.github/workflows/pr.yml")).not.toContain("OPENROUTER_API_KEY"); expect(actionUses.length).toBeGreaterThan(0); for (const uses of actionUses) { expect(uses).toMatch(/^[\w.-]+\/[\w./-]+@[0-9a-f]{40}$/); From 30e993cdb911d7f097cc296ca0fc426921d0a971 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Mon, 31 Aug 2026 05:33:21 +0000 Subject: [PATCH 16/20] fix(runtime): seal MCP failure lifecycle boundaries --- src/core/driver-command-dispatcher.ts | 32 ++++++++------ src/protocol/events/index.ts | 34 ++++++++++++++ tests/driver-external-tool-effect-v3.test.ts | 24 ++++++++++ ...driver-runtime-boundary-dispatcher.test.ts | 7 +-- tests/driver-runtime-boundary-timing.test.ts | 44 +++++++++++++++++++ 5 files changed, 122 insertions(+), 19 deletions(-) diff --git a/src/core/driver-command-dispatcher.ts b/src/core/driver-command-dispatcher.ts index 53f125d..b1fe40a 100644 --- a/src/core/driver-command-dispatcher.ts +++ b/src/core/driver-command-dispatcher.ts @@ -1,9 +1,10 @@ -import { createHash, randomUUID } from "node:crypto"; +import { randomUUID } from "node:crypto"; import { AGENT_DRIVER_MCP_EXECUTE_TIMEOUT_MS } from "../host-ports"; import { createScopedWideEvent, emitWideEvent } from "../observability"; import type { Logger } from "../observability"; import { summarizeRuntimeCommand } from "../observability/driver-debug"; +import { createMcpExecuteFailedEventIdentity } from "../protocol/events"; import type { DriverEventInput } from "../protocol/events"; import { parseRunId } from "../protocol/id"; import type { RunId } from "../protocol/id"; @@ -189,8 +190,6 @@ export class DriverCommandDispatcher { ); let joiningActiveWork = false; const onShutdown = () => { - this.#shutdownPermissionTask = this.#permissionRequests.rejectAllAndWait(); - void this.#shutdownPermissionTask.catch(() => {}); this.#abortActiveWork( socket, toErrorMessage(this.#shutdownSignal.reason, "driver.command_loop.stopped"), @@ -254,7 +253,11 @@ export class DriverCommandDispatcher { } } }); - if (this.#shutdownSignal.aborted || this.#runtimeState.isShuttingDown()) { + if ( + this.#shutdownSignal.aborted || + this.#isShuttingDown() || + this.#runtimeState.isShuttingDown() + ) { this.#abortActiveWork(socket, "driver.command_loop.stopped", "shutdown"); joiningActiveWork = true; await this.#joinActiveWork(); @@ -746,7 +749,12 @@ export class DriverCommandDispatcher { : socket.claimRunCancellation(ticket, reason); if (source !== "turn.cancel" || cancellation !== "terminal_selected") { - this.#permissionRequests.rejectAll(); + if (source === "shutdown") { + this.#shutdownPermissionTask ??= this.#permissionRequests.rejectAllAndWait(); + void this.#shutdownPermissionTask.catch(() => {}); + } else { + this.#permissionRequests.rejectAll(); + } this.#abortMcpCommands(reason); } if (cancellation === "idle") { @@ -986,25 +994,21 @@ export class DriverCommandDispatcher { } const commandFailure = toCommandFailure(command, error); - const failedPayload = { - kind: "mcp", + const failedEvent = createMcpExecuteFailedEventIdentity({ + commandId: command.commandId, rawInput: command.argumentsJson, rawOutput: commandFailure.message, - status: "failed", title: command.toolName, toolCallId: command.toolCallId, - } as const; - const failedSourceEventId = `mcp.execute.failed:${createHash("sha256") - .update(JSON.stringify([command.commandId, failedPayload])) - .digest("hex")}`; + }); await pushLosslessEvents(socket, [ { correlationId: command.commandId, kind: "tool.call.updated", - payload: failedPayload, + payload: failedEvent.payload, runId: parseRunId(command.runId), - sourceEventId: failedSourceEventId, + sourceEventId: failedEvent.sourceEventId, }, ]); diff --git a/src/protocol/events/index.ts b/src/protocol/events/index.ts index ef5187f..f1a17d1 100644 --- a/src/protocol/events/index.ts +++ b/src/protocol/events/index.ts @@ -1,3 +1,5 @@ +import { createHash } from "node:crypto"; + import { timestampSchema } from "../../contract/common"; import { parseRuntimeEventEnvelope } from "./runtime-events"; import type { RuntimeEventEnvelope, RuntimeEventInputDraft } from "./runtime-events"; @@ -22,6 +24,38 @@ export interface DriverEventEnvelope { readonly occurredAt?: string | null | undefined; } +export interface McpExecuteFailedEventIdentityInput { + readonly commandId: string; + readonly rawInput: string; + readonly rawOutput: string; + readonly title: string; + readonly toolCallId: string; +} + +export function createMcpExecuteFailedEventIdentity({ + commandId, + rawInput, + rawOutput, + title, + toolCallId, +}: McpExecuteFailedEventIdentityInput) { + const payload = { + kind: "mcp", + rawInput, + rawOutput, + status: "failed", + title, + toolCallId, + } as const; + + return { + payload, + sourceEventId: `mcp.execute.failed:${createHash("sha256") + .update(JSON.stringify([commandId, payload])) + .digest("hex")}`, + } as const; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/tests/driver-external-tool-effect-v3.test.ts b/tests/driver-external-tool-effect-v3.test.ts index 63f68dd..a3bceac 100644 --- a/tests/driver-external-tool-effect-v3.test.ts +++ b/tests/driver-external-tool-effect-v3.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; +import { createMcpExecuteFailedEventIdentity } from "../src/events"; import type { DriverEventInput } from "../src/protocol/events"; import { driverRuntimeRpcSchemas } from "../src/protocol/orpc"; import type { @@ -41,6 +42,29 @@ async function waitFor(predicate: () => boolean): Promise { } describe("durable external MCP effect protocol v3", () => { + test("exports the canonical failed MCP event identity", () => { + expect( + createMcpExecuteFailedEventIdentity({ + toolCallId: "tool-canonical", + title: "createIssue", + rawOutput: "provider rejected the request", + rawInput: '{"issue":"A-1"}', + commandId: "command-canonical", + }), + ).toEqual({ + payload: { + kind: "mcp", + rawInput: '{"issue":"A-1"}', + rawOutput: "provider rejected the request", + status: "failed", + title: "createIssue", + toolCallId: "tool-canonical", + }, + sourceEventId: + "mcp.execute.failed:807e9e02d3b2c37d2d77db8d1ad473b0db1e1e37f98c85556227baa8873ea62a", + }); + }); + test("exposes only the schema-first v3 wire", () => { const claimToken = "00000000-0000-4000-8000-000000000001"; const result = { diff --git a/tests/driver-runtime-boundary-dispatcher.test.ts b/tests/driver-runtime-boundary-dispatcher.test.ts index 7925cb6..6d8147b 100644 --- a/tests/driver-runtime-boundary-dispatcher.test.ts +++ b/tests/driver-runtime-boundary-dispatcher.test.ts @@ -528,9 +528,7 @@ describe("driver runtime boundary", () => { expect(backend.cancelledReasons).toEqual([]); expect(backend.handledInputs).toEqual([]); expect(mcpPreparations).toBe(0); - expect(permissions.hasPending()).toBe(true); - - expect(permissions.resolve("active-permission", "reject_once")).toBe(true); + expect(permissions.hasPending()).toBe(false); await expect(permission).resolves.toBe("reject_once"); }); @@ -577,8 +575,7 @@ describe("driver runtime boundary", () => { await dispatcher.run(socket, logger); expect(socket.updates.map(({ status }) => status)).toEqual(["accepted", "failed"]); - expect(permissions.hasPending()).toBeTrue(); - expect(permissions.resolve(command.requestId, "reject_once")).toBeTrue(); + expect(permissions.hasPending()).toBeFalse(); await expect(pending).resolves.toBe("reject_once"); }); diff --git a/tests/driver-runtime-boundary-timing.test.ts b/tests/driver-runtime-boundary-timing.test.ts index 48ca32e..acb2672 100644 --- a/tests/driver-runtime-boundary-timing.test.ts +++ b/tests/driver-runtime-boundary-timing.test.ts @@ -522,6 +522,50 @@ describe("driver runtime boundary", () => { ); }); + test("does not return while a shutdown predicate has active MCP work", async () => { + const execution = Promise.withResolvers(); + const entered = Promise.withResolvers(); + let shuttingDown = false; + const command = { + argumentsJson: "{}", + commandId: "mcp-before-predicate-shutdown", + kind: "mcp.execute", + requestId: "mcp-before-predicate-shutdown-request", + runId: DRIVER_TEST_IDS.runId, + serverId: "mcp-linear", + toolCallId: "tool-before-predicate-shutdown", + toolName: "waitForShutdown", + } as const; + const socket = new FakeDriverRuntimeIo([command], DRIVER_TEST_IDS.runId); + const { dispatcher, logger } = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => shuttingDown, + mcpExecute: async () => { + shuttingDown = true; + entered.resolve(); + return execution.promise; + }, + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + const run = dispatcher.run(socket, logger); + await entered.promise; + expect(await Promise.race([run.then(() => true), Bun.sleep(10).then(() => false)])).toBe(false); + + execution.resolve({ + outputText: "committed", + requestId: command.requestId, + serverId: command.serverId, + toolName: command.toolName, + }); + await run; + + expect(socket.updates.at(-1)).toMatchObject({ + commandId: command.commandId, + status: "completed", + }); + }); + test("does not report a command-loop failure when shutdown aborts an acknowledgement", async () => { const backend = createBackend(); const updateEntered = Promise.withResolvers(); From 1b4ba09164c45e916ea9ecee7be6e50757315ffd Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Mon, 31 Aug 2026 18:01:56 +0000 Subject: [PATCH 17/20] fix(runtime): make provider replay identities deterministic --- src/protocol/id/index.ts | 27 ++ .../claude/agent-sdk-driver-backend.ts | 11 +- .../claude/agent-sdk-message-state.ts | 31 ++- .../claude/agent-sdk-message-translator.ts | 6 +- src/runtimes/claude/agent-sdk-outcomes.ts | 5 +- .../claude/agent-sdk-query-options.ts | 5 +- src/runtimes/claude/agent-sdk-task-events.ts | 11 +- src/runtimes/claude/agent-sdk-tool-id.ts | 1 - .../openai/app-server-agent-task-events.ts | 8 +- .../openai/app-server-driver-backend.ts | 47 +++- .../openai/app-server-event-bridge.ts | 45 ++- src/runtimes/openai/app-server-event-state.ts | 107 +++----- src/runtimes/openai/app-server-item-events.ts | 87 +++--- .../openai/app-server-request-handler.ts | 2 +- src/runtimes/openai/event-translator.ts | 20 +- src/runtimes/runtime-public-id-state.ts | 29 -- src/runtimes/runtime-public-id.ts | 38 +++ src/runtimes/runtime-turn-transcript.ts | 35 ++- tests/claude-agent-sdk-durability.test.ts | 46 ++-- ...t-sdk-provider-fixtures-transcript.test.ts | 12 + tests/claude-agent-sdk-query-options.test.ts | 7 +- tests/claude-agent-sdk-task-events.test.ts | 2 +- tests/claude-agent-sdk-test-helpers.ts | 1 + .../cases/agent-message-completed.json | 4 +- .../cases/command-output-stream.json | 4 +- .../cases/error-before-tracked-turn.json | 2 +- .../cases/root-with-parallel-child-turns.json | 31 ++- ...rn-completed-with-final-agent-message.json | 6 +- .../cases/turn-plan-updated.json | 3 +- ...penai-app-server-agent-task-events.test.ts | 2 +- tests/openai-app-server-client.test.ts | 2 +- ...enai-app-server-event-bridge-items.test.ts | 257 ++++++++++++++++-- ...-server-event-bridge-run-lifecycle.test.ts | 10 +- ...i-app-server-event-bridge-terminal.test.ts | 49 ++-- ...penai-app-server-provider-fixtures.test.ts | 7 +- .../openai-app-server-request-handler.test.ts | 17 +- tests/openai-app-server-startup.test.ts | 11 +- tests/protocol-id.test.ts | 36 ++- 38 files changed, 712 insertions(+), 312 deletions(-) delete mode 100644 src/runtimes/claude/agent-sdk-tool-id.ts delete mode 100644 src/runtimes/runtime-public-id-state.ts create mode 100644 src/runtimes/runtime-public-id.ts diff --git a/src/protocol/id/index.ts b/src/protocol/id/index.ts index 10aa280..7c0d516 100644 --- a/src/protocol/id/index.ts +++ b/src/protocol/id/index.ts @@ -20,6 +20,7 @@ const inputDriverIdPattern = new RegExp(DRIVER_ID_INPUT_PATTERN, "u"); const driverIdAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; const driverIdRandomLength = 16; const driverIdTimeLength = 10; +const driverIdLength = driverIdTimeLength + driverIdRandomLength; const maxDriverIdTimeMs = 2 ** 48 - 1; let lastDriverIdTimeMs = -1; @@ -144,6 +145,32 @@ function incrementCrockfordBase32(value: string): string { return chars.join(""); } +export function createDriverIdFromBytes(bytes: Uint8Array): DriverId { + if (bytes.byteLength !== 16) { + throw new RangeError("Driver ID source must contain exactly 16 bytes."); + } + + let value = 0n; + for (const byte of bytes) { + value = (value << 8n) | BigInt(byte); + } + + let encoded = ""; + for (let index = 0; index < driverIdLength; index += 1) { + encoded = driverIdAlphabet.charAt(Number(value & 31n)) + encoded; + value >>= 5n; + } + return brandDriverId(encoded); +} + +export function driverIdTimeMs(id: DriverId): number { + let timeMs = 0; + for (const character of id.slice(0, driverIdTimeLength)) { + timeMs = timeMs * driverIdAlphabet.length + driverIdAlphabet.indexOf(character); + } + return timeMs; +} + export function createDriverId(): DriverId { const requestedTimeMs = assertDriverIdTimeMs(Date.now()); diff --git a/src/runtimes/claude/agent-sdk-driver-backend.ts b/src/runtimes/claude/agent-sdk-driver-backend.ts index 54d715e..0bf49a3 100644 --- a/src/runtimes/claude/agent-sdk-driver-backend.ts +++ b/src/runtimes/claude/agent-sdk-driver-backend.ts @@ -22,6 +22,7 @@ import type { RuntimeCommandInput } from "../../runtime-command"; import { raceWithAbort, settlePromiseWithTimeout } from "../../utils/async"; import type { AgentDriverBackend, AgentDriverContext } from "../../core/agent-driver-backend"; import { DriverEventPublisher } from "../driver-event-publisher"; +import { toRuntimePublicId } from "../runtime-public-id"; import { computeRuntimeBootstrapDigest, writeSkillBootstrapArtifacts } from "../skill-bootstrap"; import { readProcessEnvString, toErrorMessage } from "./agent-sdk-json"; import { ClaudeDurableEventTooLargeError } from "./agent-sdk-event-writer"; @@ -37,7 +38,6 @@ import { } from "./agent-sdk-query-options"; import { buildClaudeRecoveryPrompt } from "./agent-sdk-recovery-context"; import { readClaudeNativeResumeSessionId, requireClaudeNativeSessionId } from "./agent-sdk-resume"; -import { ClaudePublicToolCallIdState } from "./agent-sdk-tool-id"; import { drainClaudeTasks } from "./agent-sdk-tasks"; interface ActiveClaudeTurn { @@ -78,7 +78,6 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { readonly #payload: DriverStartInput; readonly #pendingProcessTasks = new Set>(); readonly #prewarm: ClaudeAgentSdkPrewarm; - readonly #publicToolCallIds = new ClaudePublicToolCallIdState(); #activeTurn: ActiveClaudeTurn | null = null; #nativeSessionId: string | null = null; #stopRequested = false; @@ -95,11 +94,11 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { createQueryOptions: this.#dependencies.createQueryOptions, getNativeSessionId: () => this.#nativeSessionId, payload, - publicToolCallId: (nativeToolCallId) => this.#publicToolCallIds.publicId(nativeToolCallId), + publicToolCallId: (nativeToolCallId) => toRuntimePublicId(nativeToolCallId, "claude-tool"), startup: async (input) => this.#dependencies.startup(input), }); this.#messageTranslator = new ClaudeAgentSdkMessageTranslator({ - publicToolCallId: (nativeToolCallId) => this.#publicToolCallIds.publicId(nativeToolCallId), + publicToolCallId: (nativeToolCallId) => toRuntimePublicId(nativeToolCallId, "claude-tool"), push: async (context, reason, events) => this.#push(context, reason, events), pushTerminal: async (context, reason, closures, terminal) => this.#eventPublisher.pushTerminal(context, reason, closures, terminal), @@ -107,6 +106,7 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { this.#recordNativeSessionId(context, sessionId), replaceNativeSessionId: async (context, previousSessionId, nextSessionId) => this.#replaceNativeSessionId(context, previousSessionId, nextSessionId), + sessionId: payload.execution.run.sessionId, }); } @@ -170,7 +170,6 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { } this.#messageTranslator.resetTurnMessageState(); - this.#publicToolCallIds.reset(); // With no native session to resume, every query starts a fresh provider // session, so the bounded platform-history replay must ride the prompt of @@ -227,7 +226,7 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { permissionTasks, processTasks, publicToolCallId: (nativeToolCallId) => - this.#publicToolCallIds.publicId(nativeToolCallId), + toRuntimePublicId(nativeToolCallId, "claude-tool"), }); queryOptionsMs = Date.now() - optionsStartedAtMs; diff --git a/src/runtimes/claude/agent-sdk-message-state.ts b/src/runtimes/claude/agent-sdk-message-state.ts index ed6a109..c40ad7c 100644 --- a/src/runtimes/claude/agent-sdk-message-state.ts +++ b/src/runtimes/claude/agent-sdk-message-state.ts @@ -1,7 +1,7 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; -import type { MessageId, RunId } from "../../protocol/id"; -import { RuntimeAssistantMessageIdIndex } from "../runtime-turn-transcript"; +import type { MessageId, RunId, SessionId } from "../../protocol/id"; +import { createRuntimeAssistantMessageId } from "../runtime-turn-transcript"; import { isRecord, readRecord, readString } from "./agent-sdk-json"; interface ClaudeAssistantFinalCandidate { @@ -38,9 +38,7 @@ export function readClaudeSdkSessionId(value: unknown): string | null { export class ClaudeAgentSdkMessageState { readonly #activeAssistantMessageIds = new Map(); readonly #activeThoughtIds = new Map(); - readonly #auxiliaryMessageIds = new RuntimeAssistantMessageIdIndex(); readonly #authoritativeAssistantMessageIds = new Set(); - readonly #assistantMessageIds = new RuntimeAssistantMessageIdIndex(); readonly #assistantMessageOrdinals = new Map(); readonly #assistantMessageRunIds = new Map(); readonly #assistantMessageSequences = new Map(); @@ -48,6 +46,7 @@ export class ClaudeAgentSdkMessageState { readonly #assistantWireAliases = new Map(); readonly #lastCompletedAssistantMessages = new Map(); readonly #pendingAssistantStreamAnchors = new Map(); + readonly #sessionId: SessionId; readonly #streamedTextMessages = new Set(); readonly #streamingNativeMessageIds = new Map(); readonly #streamingWireUuids = new Map(); @@ -55,13 +54,15 @@ export class ClaudeAgentSdkMessageState { readonly #wireAssistantMessageIds = new Map(); readonly #wireToolCallIds = new Map(); + constructor(sessionId: SessionId) { + this.#sessionId = sessionId; + } + reset(): void { this.#activeAssistantMessageIds.clear(); this.#activeThoughtIds.clear(); - this.#auxiliaryMessageIds.reset(); this.#assistantWireAliases.clear(); this.#authoritativeAssistantMessageIds.clear(); - this.#assistantMessageIds.reset(); this.#assistantMessageOrdinals.clear(); this.#assistantMessageRunIds.clear(); this.#assistantMessageSequences.clear(); @@ -81,12 +82,20 @@ export class ClaudeAgentSdkMessageState { let messageId: MessageId; if (nativeMessageId !== null) { - messageId = this.#assistantMessageIds.getOrCreate(`${runId}:native:${nativeMessageId}`); + messageId = createRuntimeAssistantMessageId( + this.#sessionId, + "claude-assistant", + `${runId}:native:${nativeMessageId}`, + ); } else if (active !== undefined) { messageId = active; } else { const ordinal = this.#nextAssistantMessageSequence(runId); - messageId = this.#assistantMessageIds.getOrCreate(`${runId}:sequence:${ordinal}`); + messageId = createRuntimeAssistantMessageId( + this.#sessionId, + "claude-assistant", + `${runId}:sequence:${ordinal}`, + ); this.#assistantMessageOrdinals.set(messageId, ordinal); } @@ -97,7 +106,11 @@ export class ClaudeAgentSdkMessageState { } auxiliaryMessageId(runId: RunId, nativeMessageId: string): MessageId { - return this.#auxiliaryMessageIds.getOrCreate(`${runId}:${nativeMessageId}`); + return createRuntimeAssistantMessageId( + this.#sessionId, + "claude-auxiliary", + `${runId}:${nativeMessageId}`, + ); } assistantMessages(): readonly (readonly [MessageId, RunId])[] { diff --git a/src/runtimes/claude/agent-sdk-message-translator.ts b/src/runtimes/claude/agent-sdk-message-translator.ts index 87a14ad..146aaf4 100644 --- a/src/runtimes/claude/agent-sdk-message-translator.ts +++ b/src/runtimes/claude/agent-sdk-message-translator.ts @@ -2,7 +2,7 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; import { jsonValueSchema } from "../../contract"; import type { DriverEventInput } from "../../protocol/events"; -import type { MessageId, RunId } from "../../protocol/id"; +import type { MessageId, RunId, SessionId } from "../../protocol/id"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; import { assertClaudeDurableEventFits, ClaudeAgentSdkEventWriter } from "./agent-sdk-event-writer"; import { @@ -36,6 +36,7 @@ import { isToolUseBlock, toToolCallId, toToolCallName, toToolResultText } from " import type { ClaudePermissionDenialAdvisory } from "./agent-sdk-outcomes"; interface ClaudeMessageTranslatorOptions { publicToolCallId(nativeToolCallId: string): string; + readonly sessionId: SessionId; push(context: AgentDriverContext, reason: string, events: DriverEventInput[]): Promise; pushTerminal( context: AgentDriverContext, @@ -69,11 +70,12 @@ export class ClaudeAgentSdkMessageTranslator { readonly #events: ClaudeAgentSdkEventWriter; readonly #options: ClaudeMessageTranslatorOptions; readonly #permissionDenialAdvisories = new Map(); - readonly #state = new ClaudeAgentSdkMessageState(); + readonly #state: ClaudeAgentSdkMessageState; constructor(options: ClaudeMessageTranslatorOptions) { this.#options = options; this.#events = new ClaudeAgentSdkEventWriter({ push: options.push }); + this.#state = new ClaudeAgentSdkMessageState(options.sessionId); } resetTurnMessageState(): void { diff --git a/src/runtimes/claude/agent-sdk-outcomes.ts b/src/runtimes/claude/agent-sdk-outcomes.ts index 9a64df6..deda4da 100644 --- a/src/runtimes/claude/agent-sdk-outcomes.ts +++ b/src/runtimes/claude/agent-sdk-outcomes.ts @@ -1,5 +1,6 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; +import { toRuntimePublicId } from "../runtime-public-id"; import { isRecord, readString } from "./agent-sdk-json"; import type { JsonObject } from "./agent-sdk-json"; @@ -102,7 +103,9 @@ export function claudePermissionDenialAdvisory( } return { - ...(message.agent_id === undefined ? {} : { agentId: message.agent_id }), + ...(message.agent_id === undefined + ? {} + : { agentId: toRuntimePublicId(message.agent_id, "claude-agent") }), ...(message.decision_reason === undefined ? {} : { decisionReason: message.decision_reason }), ...(message.decision_reason_type === undefined ? {} diff --git a/src/runtimes/claude/agent-sdk-query-options.ts b/src/runtimes/claude/agent-sdk-query-options.ts index 1b389e5..e2be11f 100644 --- a/src/runtimes/claude/agent-sdk-query-options.ts +++ b/src/runtimes/claude/agent-sdk-query-options.ts @@ -16,6 +16,7 @@ import type { AgentDriverContext } from "../../core/agent-driver-backend"; import { buildRuntimeChildProcessEnv } from "../child-process-env"; import { toMcpServerKey } from "../mcp/server-key"; import { mergeProviderOptions } from "../provider-options"; +import { toRuntimePublicId } from "../runtime-public-id"; import { buildNativeRuntimeSystemPrompt } from "../skill-bootstrap"; import { readProcessEnvString, stringifyForDisplay } from "./agent-sdk-json"; import { spawnClaudeCodeProcess } from "./agent-sdk-process"; @@ -68,7 +69,9 @@ function createCanUseTool( const decision = await context.ports.permission.request( { - ...(options.agentID === undefined ? {} : { agentId: options.agentID }), + ...(options.agentID === undefined + ? {} + : { agentId: toRuntimePublicId(options.agentID, "claude-agent") }), ...(options.blockedPath === undefined ? {} : { blockedPath: options.blockedPath }), ...(options.decisionReason === undefined ? {} diff --git a/src/runtimes/claude/agent-sdk-task-events.ts b/src/runtimes/claude/agent-sdk-task-events.ts index bd80f6d..72465c0 100644 --- a/src/runtimes/claude/agent-sdk-task-events.ts +++ b/src/runtimes/claude/agent-sdk-task-events.ts @@ -1,14 +1,12 @@ -import { createHash } from "node:crypto"; - import type { SDKBackgroundTasksChangedMessage } from "@anthropic-ai/claude-agent-sdk"; import type { DriverEventInput } from "../../protocol/events"; +import { toRuntimePublicId } from "../runtime-public-id"; import { assertClaudeDurableEventFits, ClaudeDurableEventTooLargeError, } from "./agent-sdk-event-writer"; -const MAX_CLAUDE_TASK_ID_BYTES = 256; const MAX_CLAUDE_BACKGROUND_TASK_ENTRIES = 1_024; const MAX_CLAUDE_TASK_TEXT_LENGTH = 4_096; const MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS = 256; @@ -43,13 +41,10 @@ function rejectedTaskSnapshot(code: string, taskCount: number): ClaudeBackground } function publicTaskId(nativeTaskId: string): string | null { - const bytes = Buffer.byteLength(nativeTaskId, "utf8"); - if (bytes === 0) { + if (nativeTaskId.length === 0) { return null; } - return bytes <= MAX_CLAUDE_TASK_ID_BYTES - ? nativeTaskId - : `claude-task:${createHash("sha256").update(nativeTaskId).digest("hex")}`; + return toRuntimePublicId(nativeTaskId, "claude-task"); } function boundedTaskText(value: string): string | undefined { diff --git a/src/runtimes/claude/agent-sdk-tool-id.ts b/src/runtimes/claude/agent-sdk-tool-id.ts deleted file mode 100644 index d482751..0000000 --- a/src/runtimes/claude/agent-sdk-tool-id.ts +++ /dev/null @@ -1 +0,0 @@ -export { RuntimePublicIdState as ClaudePublicToolCallIdState } from "../runtime-public-id-state"; diff --git a/src/runtimes/openai/app-server-agent-task-events.ts b/src/runtimes/openai/app-server-agent-task-events.ts index 4386778..722ca65 100644 --- a/src/runtimes/openai/app-server-agent-task-events.ts +++ b/src/runtimes/openai/app-server-agent-task-events.ts @@ -1,6 +1,5 @@ -import { createHash } from "node:crypto"; - import type { DriverEventInput } from "../../protocol/events"; +import { toRuntimePublicId } from "../runtime-public-id"; import { assertOpenAiDurableEventFits, MAX_OPENAI_DURABLE_EVENT_BYTES, @@ -9,7 +8,6 @@ import { const MAX_OPENAI_ACTIVE_AGENT_TASKS = 1_024; const MAX_OPENAI_CLOSED_AGENT_TASKS = 1_024; const MAX_OPENAI_VISIBLE_AGENT_TASKS = 256; -const MAX_OPENAI_AGENT_TASK_ID_BYTES = 256; const MAX_OPENAI_AGENT_TASK_TEXT_LENGTH = 4_096; const OPENAI_AGENT_TASK_EVENT_ENVELOPE_RESERVE_BYTES = 1_024; @@ -31,9 +29,7 @@ interface OpenAiAgentTaskUpdate { } export function toOpenAiAgentTaskId(nativeTaskId: string): string { - return Buffer.byteLength(nativeTaskId, "utf8") <= MAX_OPENAI_AGENT_TASK_ID_BYTES - ? nativeTaskId - : `openai-task:${createHash("sha256").update(nativeTaskId).digest("hex")}`; + return toRuntimePublicId(nativeTaskId, "openai-thread"); } function boundedTaskText(value: string): string | undefined { diff --git a/src/runtimes/openai/app-server-driver-backend.ts b/src/runtimes/openai/app-server-driver-backend.ts index 43f29a8..2d6fb08 100644 --- a/src/runtimes/openai/app-server-driver-backend.ts +++ b/src/runtimes/openai/app-server-driver-backend.ts @@ -10,13 +10,14 @@ import { toDurationMs, } from "../../core/driver-runtime-timing"; import { summarizePath, summarizeRuntimeCommandInput } from "../../observability/driver-debug"; -import type { RunId } from "../../protocol/id"; +import { driverIdTimeMs, type RunId } from "../../protocol/id"; import type { DriverRuntime } from "../../protocol/runtime"; import type { DriverStartInput } from "../../protocol/start"; import type { RuntimeCommandInput } from "../../runtime-command"; import { raceWithAbort, settlePromiseWithTimeout } from "../../utils/async"; import type { AgentDriverBackend, AgentDriverContext } from "../../core/agent-driver-backend"; import { DriverEventPublisher } from "../driver-event-publisher"; +import { createRuntimeSourceEventId } from "../runtime-public-id"; import { buildNativeRuntimeSystemPrompt, computeRuntimeBootstrapDigest, @@ -574,7 +575,6 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { ], runId, sessionId: context.payload.execution.run.sessionId, - sourceEventId: `openai.provider.turn_start:${publicTurnId}`, stage: "driver_turn", startedAt: new Date(turnStartRequestedAtMs).toISOString(), native: { @@ -665,6 +665,11 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { error: protocolError, kind: "failed", }); + const closureSourcePrefix = createRuntimeSourceEventId( + "openai.provider.failed.closure", + "run", + runId, + ); await this.#eventPublisher.pushTerminal( context, @@ -673,12 +678,17 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { { ...taskClosure, runId, - sourceEventId: `openai.provider.failed.closure:${runId}:0`, + sourceEventId: createRuntimeSourceEventId( + "openai.derived", + closureSourcePrefix, + 0, + JSON.stringify({ ...taskClosure, runId }), + ), }, { kind: "run.started", payload: { - startedAt: new Date().toISOString(), + startedAt: new Date(driverIdTimeMs(runId)).toISOString(), }, runId, sourceEventId: `openai.provider.failed.started:${runId}`, @@ -687,7 +697,13 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { ...event, runId, sourceEventId: - event.sourceEventId ?? `openai.provider.failed.closure:${runId}:${String(index + 1)}`, + event.sourceEventId ?? + createRuntimeSourceEventId( + "openai.derived", + closureSourcePrefix, + index + 1, + JSON.stringify({ ...event, runId }), + ), })), ], { @@ -713,6 +729,11 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { const [taskClosure, ...itemClosures] = this.#events.turnStartTerminalEvents({ kind: "cancelled", }); + const closureSourcePrefix = createRuntimeSourceEventId( + "openai.turn_start.cancelled.closure", + "run", + runId, + ); await this.#eventPublisher.pushTerminal( context, "driver.openai.turn_start.cancelled", @@ -720,12 +741,17 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { { ...taskClosure, runId, - sourceEventId: `openai.turn_start.cancelled.closure:${runId}:0`, + sourceEventId: createRuntimeSourceEventId( + "openai.derived", + closureSourcePrefix, + 0, + JSON.stringify({ ...taskClosure, runId }), + ), }, { kind: "run.started", payload: { - startedAt: new Date().toISOString(), + startedAt: new Date(driverIdTimeMs(runId)).toISOString(), }, runId, sourceEventId: `openai.turn_start.cancelled.started:${runId}`, @@ -745,7 +771,12 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { runId, sourceEventId: event.sourceEventId ?? - `openai.turn_start.cancelled.closure:${runId}:${String(index + 1)}`, + createRuntimeSourceEventId( + "openai.derived", + closureSourcePrefix, + index + 1, + JSON.stringify({ ...event, runId }), + ), })), ], { diff --git a/src/runtimes/openai/app-server-event-bridge.ts b/src/runtimes/openai/app-server-event-bridge.ts index e70c08d..71cfbe1 100644 --- a/src/runtimes/openai/app-server-event-bridge.ts +++ b/src/runtimes/openai/app-server-event-bridge.ts @@ -1,17 +1,17 @@ import { DriverTurnCancelledError } from "../../core/driver-runtime-state"; import type { DriverEventInput } from "../../protocol/events"; -import { createDriverId, type RunId } from "../../protocol/id"; +import { createDriverId, driverIdTimeMs, type RunId } from "../../protocol/id"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; import { toOpenAiProtocolError } from "./app-server-event-mapping"; import { OpenAiItemState, OpenAiMessageState, OpenAiPlanState, - OpenAiPublicIdState, OpenAiSessionUsageState, OpenAiToolState, type OpenAiTerminalOutcome, } from "./app-server-event-state"; +import { createRuntimeSourceEventId, toRuntimePublicId } from "../runtime-public-id"; import { chunkOpenAiText, OpenAiAppServerItemEventBridge } from "./app-server-item-events"; import { isRecord, readArray, readNonEmptyString, readRecord, readString } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; @@ -141,13 +141,28 @@ function turnClosureEvents(input: { publicTurnId: string; runId?: RunId | undefined; }): DriverEventInput[] { - return input.events.map((event, index) => ({ - ...event, - ...(event.runId === undefined && input.runId !== undefined ? { runId: input.runId } : {}), - sourceEventId: - event.sourceEventId ?? - `openai.${input.eventName}.closure:${input.publicTurnId}:${String(index)}`, - })); + const sourcePrefix = createRuntimeSourceEventId( + `openai.${input.eventName}.closure`, + "turn", + input.publicTurnId, + ); + return input.events.map((event, index) => { + const scopedEvent = { + ...event, + ...(event.runId === undefined && input.runId !== undefined ? { runId: input.runId } : {}), + }; + return { + ...scopedEvent, + sourceEventId: + event.sourceEventId ?? + createRuntimeSourceEventId( + "openai.derived", + sourcePrefix, + index, + JSON.stringify(scopedEvent), + ), + }; + }); } export class OpenAiAppServerEventBridge { @@ -156,7 +171,6 @@ export class OpenAiAppServerEventBridge { readonly #messages = new OpenAiMessageState(); readonly #options: OpenAiAppServerEventBridgeOptions; readonly #plans = new OpenAiPlanState(); - readonly #publicTurnIds = new OpenAiPublicIdState(); readonly #tools = new OpenAiToolState(); readonly #turnStarts = new Map>(); readonly #turns = new OpenAiTurnTracker(); @@ -188,7 +202,6 @@ export class OpenAiAppServerEventBridge { clearActiveTurns(): void { this.#turnStarts.clear(); - this.#publicTurnIds.reset(); this.#turns.clearActiveTurns(); this.#itemEvents.reset(); this.#usage.reset(); @@ -606,7 +619,7 @@ export class OpenAiAppServerEventBridge { async publishNativeResumeRef(context: AgentDriverContext): Promise { const threadId = this.#options.requireThreadId(); - const publicThreadId = this.#publicTurnIds.publicId(threadId, "thread"); + const publicThreadId = toRuntimePublicId(threadId, "openai-thread"); await this.#push(context, "driver.openai.native_resume_ref.updated", [ { kind: "runtime.resume.updated", @@ -623,6 +636,10 @@ export class OpenAiAppServerEventBridge { context: AgentDriverContext, input: { runId?: RunId | undefined; turnId: string }, ): Promise { + const identityRunId = input.runId ?? context.ports.eventSink.currentRunId(); + if (identityRunId === null) { + return; + } if (this.#turns.hasTurnStarted(input.turnId)) { return; } @@ -643,7 +660,7 @@ export class OpenAiAppServerEventBridge { }), kind: "run.started", payload: { - startedAt: new Date().toISOString(), + startedAt: new Date(driverIdTimeMs(identityRunId)).toISOString(), }, }, ]); @@ -828,7 +845,7 @@ export class OpenAiAppServerEventBridge { ...toBoundedOpenAiTelemetry({ processId }, ["processId"]), itemId: publicItemId, status: "running", - threadId: this.#publicTurnIds.publicId(threadId, "thread"), + threadId: toRuntimePublicId(threadId, "openai-thread"), turnId: this.publicTurnId(turnId), }, }, diff --git a/src/runtimes/openai/app-server-event-state.ts b/src/runtimes/openai/app-server-event-state.ts index 7bb33f2..74cf914 100644 --- a/src/runtimes/openai/app-server-event-state.ts +++ b/src/runtimes/openai/app-server-event-state.ts @@ -2,14 +2,12 @@ import type { DriverEventInput } from "../../protocol/events"; import type { MessageId } from "../../protocol/id"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; import type { ProtocolError } from "../../contract"; -import { RuntimePublicIdState } from "../runtime-public-id-state"; -import { RuntimeAssistantMessageIdIndex } from "../runtime-turn-transcript"; +import { createRuntimeSourceEventId, toRuntimePublicId } from "../runtime-public-id"; +import { createRuntimeAssistantMessageId } from "../runtime-turn-transcript"; import { toOpenAiSessionUsageSummary } from "./app-server-event-mapping"; import { readRecord } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; -export { RuntimePublicIdState as OpenAiPublicIdState } from "../runtime-public-id-state"; - export type OpenAiEventPush = ( context: AgentDriverContext, reason: string, @@ -140,21 +138,16 @@ export class OpenAiMessageState { >(); readonly #ended = new Set(); readonly #itemSequences = new Map(); - readonly #itemByMessageId = new Map(); readonly #itemMessageIds = new Map(); readonly #reasoningEnded = new Set(); - readonly #reasoningIds = new RuntimeAssistantMessageIdIndex(); readonly #reasoningParts = new Map>(); readonly #reasoningStarted = new Set(); readonly #reasoningTextById = new Map(); readonly #starting = new Map>(); readonly #started = new Set(); readonly #textById = new Map(); - readonly #turnByMessageId = new Map(); - readonly #turnMessages = new RuntimeAssistantMessageIdIndex(); - readonly #turnMessageIds = new Map(); - readonly #turnMessageSequences = new Map(); readonly #turnNextItemSequences = new Map(); + readonly #turnParentMessageIds = new Map(); appendText(messageId: MessageId, delta: string): void { if (delta.length === 0 || this.#ended.has(messageId)) { @@ -263,19 +256,20 @@ export class OpenAiMessageState { turnId: string, push: OpenAiEventPush, ): Promise { - const existing = this.#turnMessageIds.get(turnId); + const existing = this.#turnParentMessageIds.get(turnId); if (existing !== undefined) { await this.ensureStarted(context, existing, push); return existing; } - const nextSequence = (this.#turnMessageSequences.get(turnId) ?? 0) + 1; - const generated = this.#turnMessages.getOrCreate(`${turnId}:${nextSequence}`); + const generated = createRuntimeAssistantMessageId( + context.payload.execution.run.sessionId, + "openai-message", + `turn:${JSON.stringify(turnId)}`, + ); await this.ensureStarted(context, generated, push); - this.#turnMessageSequences.set(turnId, nextSequence); - this.#turnMessageIds.set(turnId, generated); - this.#turnByMessageId.set(generated, turnId); + this.#turnParentMessageIds.set(turnId, generated); return generated; } @@ -284,31 +278,23 @@ export class OpenAiMessageState { input: { itemId: string; turnId: string }, push: OpenAiEventPush, ): Promise { - const itemKey = `${input.turnId}:${input.itemId}`; + const itemKey = JSON.stringify([input.turnId, input.itemId]); const existing = this.#itemMessageIds.get(itemKey); if (existing !== undefined) { - if (!this.#ended.has(existing)) { - this.#turnMessageIds.set(input.turnId, existing); - } await this.ensureStarted(context, existing, push); return existing; } - const activeMessageId = this.#turnMessageIds.get(input.turnId); - const messageId = - activeMessageId !== undefined && - !this.#ended.has(activeMessageId) && - !this.#itemByMessageId.has(activeMessageId) - ? activeMessageId - : this.#turnMessages.getOrCreate(`item:${itemKey}`); + const messageId = createRuntimeAssistantMessageId( + context.payload.execution.run.sessionId, + "openai-message", + `item:${itemKey}`, + ); await this.ensureStarted(context, messageId, push); this.#observeItem(itemKey, input.turnId); this.#itemMessageIds.set(itemKey, messageId); - this.#itemByMessageId.set(messageId, itemKey); - this.#turnMessageIds.set(input.turnId, messageId); - this.#turnByMessageId.set(messageId, input.turnId); return messageId; } @@ -319,7 +305,7 @@ export class OpenAiMessageState { text: string; turnId: string; }): boolean { - const itemKey = `${input.turnId}:${input.itemId}`; + const itemKey = JSON.stringify([input.turnId, input.itemId]); const sequence = this.#observeItem(itemKey, input.turnId); if (!this.needsSnapshot(input)) { return false; @@ -336,7 +322,7 @@ export class OpenAiMessageState { text: string; turnId: string; }): boolean { - const previous = this.#completedSnapshots.get(`${input.turnId}:${input.itemId}`); + const previous = this.#completedSnapshots.get(JSON.stringify([input.turnId, input.itemId])); return ( previous?.messageId !== input.messageId || previous.phase !== input.phase || @@ -425,16 +411,6 @@ export class OpenAiMessageState { this.#ended.add(messageId); this.#started.delete(messageId); - const turnId = this.#turnByMessageId.get(messageId); - - if (turnId !== undefined) { - this.#turnByMessageId.delete(messageId); - - if (this.#turnMessageIds.get(turnId) === messageId) { - this.#turnMessageIds.delete(turnId); - } - } - return true; } @@ -446,8 +422,12 @@ export class OpenAiMessageState { return this.#reasoningEnded.has(messageId); } - reasoningId(itemId: string): string { - return this.#reasoningIds.getOrCreate(itemId); + reasoningId(context: AgentDriverContext, itemId: string): MessageId { + return createRuntimeAssistantMessageId( + context.payload.execution.run.sessionId, + "openai-reasoning", + itemId, + ); } isReasoningStarted(messageId: string): boolean { @@ -465,28 +445,23 @@ export class OpenAiMessageState { } messageForTurn(turnId: string): MessageId | null { - return this.#turnMessageIds.get(turnId) ?? null; + return this.#turnParentMessageIds.get(turnId) ?? null; } reset(): void { this.#completedSnapshots.clear(); this.#ended.clear(); this.#itemSequences.clear(); - this.#itemByMessageId.clear(); this.#itemMessageIds.clear(); this.#reasoningEnded.clear(); - this.#reasoningIds.reset(); this.#reasoningParts.clear(); this.#reasoningStarted.clear(); this.#reasoningTextById.clear(); this.#starting.clear(); this.#started.clear(); this.#textById.clear(); - this.#turnByMessageId.clear(); - this.#turnMessages.reset(); - this.#turnMessageIds.clear(); - this.#turnMessageSequences.clear(); this.#turnNextItemSequences.clear(); + this.#turnParentMessageIds.clear(); } #observeItem(itemKey: string, turnId: string): number { @@ -505,7 +480,6 @@ export class OpenAiMessageState { export class OpenAiItemState { readonly #completed = new Set(); - readonly #ids = new RuntimePublicIdState(); isCompleted(itemId: string): boolean { return this.#completed.has(itemId); @@ -521,12 +495,11 @@ export class OpenAiItemState { } publicId(nativeId: string, namespace: "item" | "turn" = "item"): string { - return this.#ids.publicId(nativeId, namespace); + return toRuntimePublicId(nativeId, namespace === "item" ? "openai-item" : "openai-turn"); } reset(): void { this.#completed.clear(); - this.#ids.reset(); } } @@ -578,7 +551,11 @@ export class OpenAiToolState { parentMessageId: input.parentMessageId, title: input.toolCallName, }, - sourceEventId: `openai.item.started:${input.sourceScope}:${input.publicToolCallId}`, + sourceEventId: createRuntimeSourceEventId( + "openai.item.started", + input.sourceScope, + input.publicToolCallId, + ), }, { kind: "tool.call.updated", @@ -589,7 +566,11 @@ export class OpenAiToolState { title: input.toolCallName, toolCallId: input.publicToolCallId, }, - sourceEventId: `openai.tool.started:${input.sourceScope}:${input.publicToolCallId}`, + sourceEventId: createRuntimeSourceEventId( + "openai.tool.started", + input.sourceScope, + input.publicToolCallId, + ), }, ]; for (const event of events) { @@ -653,11 +634,6 @@ export class OpenAiToolState { export class OpenAiPlanState { readonly #plans = new Map(); - readonly #turnVersions = new Map(); - - currentLength(itemId: string): number { - return this.#plans.get(itemId)?.content.length ?? 0; - } appendDelta(itemId: string, delta: string): void { const current = this.#plans.get(itemId); @@ -710,14 +686,5 @@ export class OpenAiPlanState { reset(): void { this.#plans.clear(); - this.#turnVersions.clear(); - } - - turnVersion(turnId: string): number { - return this.#turnVersions.get(turnId) ?? 0; - } - - advanceTurnVersion(turnId: string): void { - this.#turnVersions.set(turnId, this.turnVersion(turnId) + 1); } } diff --git a/src/runtimes/openai/app-server-item-events.ts b/src/runtimes/openai/app-server-item-events.ts index fd93c75..95ed2d1 100644 --- a/src/runtimes/openai/app-server-item-events.ts +++ b/src/runtimes/openai/app-server-item-events.ts @@ -1,6 +1,7 @@ import type { AgentDriverContext } from "../../core/agent-driver-backend"; import type { DriverEventInput } from "../../protocol/events"; import { chunkJsonText } from "../provider-json"; +import { createRuntimeSourceEventId, toRuntimePublicId } from "../runtime-public-id"; import { toOpenAiPlanStatus } from "./app-server-event-mapping"; import { assertOpenAiDurableEventFits, @@ -130,10 +131,20 @@ function withOpenAiEventIds( events: readonly DriverEventInput[], sourcePrefix: string, ): DriverEventInput[] { - return events.map((event, index) => ({ - ...event, - sourceEventId: event.sourceEventId ?? `${sourcePrefix}:${String(index)}`, - })); + let autoIndex = 0; + return events.map((event) => + typeof event.sourceEventId !== "string" || event.sourceEventId.length === 0 + ? { + ...event, + sourceEventId: createRuntimeSourceEventId( + "openai.derived", + sourcePrefix, + autoIndex++, + JSON.stringify(event), + ), + } + : event, + ); } function toOpenAiItemLifecycleEvents( @@ -186,7 +197,7 @@ function toOpenAiItemLifecycleEvents( status: activityKind === "interrupted" ? "cancelled" : "completed", }), activityKind, - agentId, + agentId: toOpenAiAgentTaskId(agentId), agentPath, taskId: toOpenAiAgentTaskId(agentId), title: `Sub-agent ${activityKind}`, @@ -494,7 +505,7 @@ export class OpenAiAppServerItemEventBridge { if (events.length > 0) { const durableEvents = withOpenAiEventIds( events, - `openai.item.completed:${publicTurnId}:${publicItemId}`, + createRuntimeSourceEventId("openai.item.completed", publicTurnId, publicItemId), ); for (const event of durableEvents) { assertOpenAiDurableEventFits(event, `item completion ${publicItemId}`); @@ -525,7 +536,7 @@ export class OpenAiAppServerItemEventBridge { const publicTurnId = this.#items.publicId(turnId, "turn"); const events = withOpenAiEventIds( toOpenAiItemLifecycleEvents(item, publicItemId, "completed"), - `openai.item.completed:${publicTurnId}:${publicItemId}`, + createRuntimeSourceEventId("openai.item.completed", publicTurnId, publicItemId), ); for (const event of events) { assertOpenAiDurableEventFits(event, `post-terminal sub-agent activity ${publicItemId}`); @@ -554,7 +565,7 @@ export class OpenAiAppServerItemEventBridge { if (toolName === null) { const events = withOpenAiEventIds( toOpenAiItemLifecycleEvents(item, publicItemId, "started"), - `openai.item.started:${publicTurnId}:${publicItemId}`, + createRuntimeSourceEventId("openai.item.started", publicTurnId, publicItemId), ); if (events.length > 0) { @@ -587,14 +598,8 @@ export class OpenAiAppServerItemEventBridge { return; } const publicItemId = this.#items.publicId(itemId); - const publicTurnId = this.#items.publicId(turnId, "turn"); - const event = { - ...this.#plans.createDeltaEvent(itemId, delta), - sourceEventId: `openai.plan.delta:${publicTurnId}:${publicItemId}:${String( - this.#plans.currentLength(itemId), - )}`, - }; + const event = this.#plans.createDeltaEvent(itemId, delta); assertOpenAiDurableEventFits(event, `plan update ${publicItemId}`); await this.#push(context, "driver.openai.plan.delta", [event]); this.#plans.appendDelta(itemId, delta); @@ -608,14 +613,13 @@ export class OpenAiAppServerItemEventBridge { return; } - const messageId = this.#messages.reasoningId(itemId); + const messageId = this.#messages.reasoningId(context, itemId); const events: DriverEventInput[] = []; if (this.#messages.isReasoningEnded(messageId)) { return; } - const currentLength = this.#messages.currentReasoningText(messageId).length; this.#messages.ensureReasoning(messageId, events, false); events.push( ...chunkOpenAiText(delta).map((contentDelta): DriverEventInput => ({ @@ -629,11 +633,7 @@ export class OpenAiAppServerItemEventBridge { })), ); - await this.#push( - context, - "driver.openai.reasoning.summary", - withOpenAiEventIds(events, `openai.reasoning.delta:${messageId}:${String(currentLength)}`), - ); + await this.#push(context, "driver.openai.reasoning.summary", events); this.#messages.ensureReasoning(messageId, [], true); this.#messages.appendReasoningText(messageId, delta); } @@ -648,13 +648,21 @@ export class OpenAiAppServerItemEventBridge { typeof summaryIndex !== "number" || !Number.isInteger(summaryIndex) || summaryIndex < 1 || - !this.#messages.beginReasoningPart(this.#messages.reasoningId(itemId), summaryIndex, false) + !this.#messages.beginReasoningPart( + this.#messages.reasoningId(context, itemId), + summaryIndex, + false, + ) ) { return; } await this.onReasoningDelta(context, { ...params, delta: "\n\n" }); - this.#messages.beginReasoningPart(this.#messages.reasoningId(itemId), summaryIndex, true); + this.#messages.beginReasoningPart( + this.#messages.reasoningId(context, itemId), + summaryIndex, + true, + ); } async onToolOutput(context: AgentDriverContext, params: JsonObject): Promise { @@ -778,7 +786,11 @@ export class OpenAiAppServerItemEventBridge { }; if (this.#messages.needsSnapshot(snapshot)) { - const sourcePrefix = `openai.turn.final_message:${publicTurnId}:${publicItemId}`; + const sourcePrefix = createRuntimeSourceEventId( + "openai.turn.final_message", + publicTurnId, + publicItemId, + ); const snapshotEvents = toOpenAiMessageSnapshotEvents({ item: finalAssistantItem, itemId, @@ -844,13 +856,9 @@ export class OpenAiAppServerItemEventBridge { entries: plan, source: "driver", }, - sourceEventId: `openai.turn.plan:${this.#items.publicId(turnId, "turn")}:${String( - this.#plans.turnVersion(turnId), - )}`, }; assertOpenAiDurableEventFits(event, "turn plan update"); await this.#push(context, "driver.openai.turn.plan.updated", [event]); - this.#plans.advanceTurnVersion(turnId); } async #prepareMessageEnd( @@ -904,7 +912,11 @@ export class OpenAiAppServerItemEventBridge { itemId, messageId, phase: toOpenAiMessagePhase(item), - sourcePrefix: `openai.item.completed:${publicTurnId}:${publicItemId}`, + sourcePrefix: createRuntimeSourceEventId( + "openai.item.completed", + publicTurnId, + publicItemId, + ), text: filteredFinalText.text, }), ); @@ -1037,9 +1049,11 @@ export class OpenAiAppServerItemEventBridge { const summary = Array.isArray(item["summary"]) ? item["summary"].filter((entry): entry is string => typeof entry === "string") : []; - const messageId = this.#messages.reasoningId(itemId); + const messageId = this.#messages.reasoningId(context, itemId); const summaryText = summary.join("\n\n"); const currentText = this.#messages.currentReasoningText(messageId); + // A terminal turn snapshot can be the first frame seen by a fresh Driver. + // Active-run Driver loss is terminal, so only a live instance can have a durable prefix to skip. const missingText = summaryText.startsWith(currentText) ? summaryText.slice(currentText.length) : currentText.length === 0 @@ -1062,7 +1076,7 @@ export class OpenAiAppServerItemEventBridge { if (missingText.length > 0) { this.#messages.ensureReasoning(messageId, events, false); events.push( - ...chunkOpenAiText(missingText).map((contentDelta, index): DriverEventInput => ({ + ...chunkOpenAiText(missingText).map((contentDelta): DriverEventInput => ({ delivery: "lossless", kind: "thought.delta", payload: { @@ -1070,7 +1084,6 @@ export class OpenAiAppServerItemEventBridge { contentDelta, thoughtId: messageId, }, - sourceEventId: `openai.reasoning.completed:${messageId}:${String(index)}`, })), ); } @@ -1153,7 +1166,7 @@ export class OpenAiAppServerItemEventBridge { kind: "tool.call.updated", payload: { ...(itemType === "collabAgentToolCall" && receiverThreadIds.length === 1 - ? { agentId: receiverThreadIds[0] } + ? { agentId: toRuntimePublicId(receiverThreadIds[0]!, "openai-thread") } : {}), ...(toolResult === null || toolResult.length === 0 || @@ -1169,7 +1182,11 @@ export class OpenAiAppServerItemEventBridge { status: terminalStatus, toolCallId: publicItemId, }, - sourceEventId: `openai.item.completed:${publicTurnId}:${publicItemId}:0`, + sourceEventId: `${createRuntimeSourceEventId( + "openai.item.completed", + publicTurnId, + publicItemId, + )}:0`, }); assertOpenAiDurableEventFits(events[0]!, `tool completion ${publicItemId}`); events.push({ diff --git a/src/runtimes/openai/app-server-request-handler.ts b/src/runtimes/openai/app-server-request-handler.ts index a7d70a7..780abf5 100644 --- a/src/runtimes/openai/app-server-request-handler.ts +++ b/src/runtimes/openai/app-server-request-handler.ts @@ -170,7 +170,7 @@ export class OpenAiAppServerRequestHandler { signal: AbortSignal, ): Promise { const payload = isRecord(params) ? params : {}; - const requestId = `${method}:${String(id)}`; + const requestId = `${method}:${typeof id}:${String(id)}`; switch (method) { case "currentTime/read": { diff --git a/src/runtimes/openai/event-translator.ts b/src/runtimes/openai/event-translator.ts index 3f6d231..6b30214 100644 --- a/src/runtimes/openai/event-translator.ts +++ b/src/runtimes/openai/event-translator.ts @@ -1,4 +1,5 @@ import type { DriverEventInput } from "../../protocol/events"; +import { toRuntimePublicId } from "../runtime-public-id"; import { isRecord, readArray, @@ -95,15 +96,24 @@ export function toOpenAiCollaborationOutput(item: JsonObject): JsonObject | null return null; } + const agentsStates = readRecord(item, "agentsStates") ?? {}; + const senderThreadId = readString(item, "senderThreadId"); + return { - agentsStates: readRecord(item, "agentsStates") ?? {}, + agentsStates: Object.fromEntries( + Object.entries(agentsStates).map(([threadId, state]) => [ + toRuntimePublicId(threadId, "openai-thread"), + state, + ]), + ), model: readString(item, "model"), prompt: readString(item, "prompt"), reasoningEffort: readString(item, "reasoningEffort"), - receiverThreadIds: readArray(item, "receiverThreadIds").filter( - (entry): entry is string => typeof entry === "string", - ), - senderThreadId: readString(item, "senderThreadId"), + receiverThreadIds: readArray(item, "receiverThreadIds") + .filter((entry): entry is string => typeof entry === "string") + .map((threadId) => toRuntimePublicId(threadId, "openai-thread")), + senderThreadId: + senderThreadId === null ? null : toRuntimePublicId(senderThreadId, "openai-thread"), status: readString(item, "status"), tool: readString(item, "tool"), }; diff --git a/src/runtimes/runtime-public-id-state.ts b/src/runtimes/runtime-public-id-state.ts deleted file mode 100644 index 9118613..0000000 --- a/src/runtimes/runtime-public-id-state.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createHash } from "node:crypto"; - -import { createDriverId } from "../protocol/id"; - -const MAX_PUBLIC_NATIVE_ID_BYTES = 256; - -export class RuntimePublicIdState { - readonly #publicIds = new Map(); - - publicId(nativeId: string, namespace = "id"): string { - if (Buffer.byteLength(nativeId, "utf8") <= MAX_PUBLIC_NATIVE_ID_BYTES) { - return nativeId; - } - - const key = `${namespace}:${createHash("sha256").update(nativeId).digest("hex")}`; - const existing = this.#publicIds.get(key); - if (existing !== undefined) { - return existing; - } - - const publicId = createDriverId(); - this.#publicIds.set(key, publicId); - return publicId; - } - - reset(): void { - this.#publicIds.clear(); - } -} diff --git a/src/runtimes/runtime-public-id.ts b/src/runtimes/runtime-public-id.ts new file mode 100644 index 0000000..b0e4adc --- /dev/null +++ b/src/runtimes/runtime-public-id.ts @@ -0,0 +1,38 @@ +import { createHash } from "node:crypto"; + +const MAX_PUBLIC_NATIVE_ID_BYTES = 256; +const HASHED_PUBLIC_ID_PATTERN = /^rid1_[A-Za-z0-9_-]{43}$/u; +const HASH_DOMAIN = "mosoo.runtime-public-id/v1"; +const SOURCE_EVENT_HASH_DOMAIN = "mosoo.runtime-source-event-id/v1"; + +type RuntimePublicIdNamespace = + | "claude-agent" + | "claude-task" + | "claude-tool" + | "openai-item" + | "openai-thread" + | "openai-turn"; + +export function toRuntimePublicId(nativeId: string, namespace: RuntimePublicIdNamespace): string { + if ( + Buffer.byteLength(nativeId, "utf8") <= MAX_PUBLIC_NATIVE_ID_BYTES && + !HASHED_PUBLIC_ID_PATTERN.test(nativeId) + ) { + return nativeId; + } + + const digest = createHash("sha256") + .update(JSON.stringify([HASH_DOMAIN, namespace, nativeId])) + .digest("base64url"); + return `rid1_${digest}`; +} + +export function createRuntimeSourceEventId( + scope: string, + ...identity: readonly (number | string)[] +): string { + const digest = createHash("sha256") + .update(JSON.stringify([SOURCE_EVENT_HASH_DOMAIN, scope, ...identity])) + .digest("base64url"); + return `${scope}:sid1_${digest}`; +} diff --git a/src/runtimes/runtime-turn-transcript.ts b/src/runtimes/runtime-turn-transcript.ts index f6afea3..5c7de2a 100644 --- a/src/runtimes/runtime-turn-transcript.ts +++ b/src/runtimes/runtime-turn-transcript.ts @@ -1,22 +1,21 @@ -import { createDriverId } from "../protocol/id"; -import type { MessageId } from "../protocol/id"; +import { createHash } from "node:crypto"; -export class RuntimeAssistantMessageIdIndex { - readonly #messageIds = new Map(); +import { createDriverIdFromBytes } from "../protocol/id"; +import type { MessageId, SessionId } from "../protocol/id"; - getOrCreate(key: TKey): MessageId { - const existing = this.#messageIds.get(key); +type RuntimeAssistantMessageNamespace = + | "claude-assistant" + | "claude-auxiliary" + | "openai-message" + | "openai-reasoning"; - if (existing !== undefined) { - return existing; - } - - const messageId = createDriverId() as MessageId; - this.#messageIds.set(key, messageId); - return messageId; - } - - reset(): void { - this.#messageIds.clear(); - } +export function createRuntimeAssistantMessageId( + sessionId: SessionId, + namespace: RuntimeAssistantMessageNamespace, + key: string, +): MessageId { + const digest = createHash("sha256") + .update(JSON.stringify(["mosoo.runtime-assistant-message-id/v1", sessionId, namespace, key])) + .digest(); + return createDriverIdFromBytes(digest.subarray(0, 16)) as MessageId; } diff --git a/tests/claude-agent-sdk-durability.test.ts b/tests/claude-agent-sdk-durability.test.ts index 0a43383..cfd5b85 100644 --- a/tests/claude-agent-sdk-durability.test.ts +++ b/tests/claude-agent-sdk-durability.test.ts @@ -13,7 +13,7 @@ import { toRuntimeEventInput } from "../src/runtime-events"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { ClaudeDurableEventTooLargeError } from "../src/runtimes/claude/agent-sdk-event-writer"; import { ClaudeAgentSdkMessageTranslator } from "../src/runtimes/claude/agent-sdk-message-translator"; -import { ClaudePublicToolCallIdState } from "../src/runtimes/claude/agent-sdk-tool-id"; +import { toRuntimePublicId } from "../src/runtimes/runtime-public-id"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; import { CMA_MAX_EVENT_BYTES, encodeCmaSseRecord } from "../src/stores/cma-store"; import { createCmaMemoryStore } from "../src/stores/memory"; @@ -46,9 +46,9 @@ function successResult(input: { } as unknown as SDKMessage; } -function createCmaHarness() { - const runId = createDriverId() as RunId; - const sessionId = createDriverId() as SessionId; +function createCmaHarness(ids: { readonly runId?: RunId; readonly sessionId?: SessionId } = {}) { + const runId = ids.runId ?? (createDriverId() as RunId); + const sessionId = ids.sessionId ?? (createDriverId() as SessionId); const events: DriverEventInput[] = []; const sseFrameBytes: number[] = []; const context = createAgentDriverContext({ @@ -78,16 +78,16 @@ function createCmaHarness() { sseFrameBytes.push(...records.map((record) => encodeCmaSseRecord(record).byteLength)); } }; - const toolCallIds = new ClaudePublicToolCallIdState(); const translator = new ClaudeAgentSdkMessageTranslator({ - publicToolCallId: (nativeToolCallId) => toolCallIds.publicId(nativeToolCallId), + publicToolCallId: (nativeToolCallId) => toRuntimePublicId(nativeToolCallId, "claude-tool"), push: async (_context, _reason, batch) => append(batch), pushTerminal: async (_context, _reason, closures, terminal) => append([...closures, terminal]), recordNativeSessionId: async () => {}, replaceNativeSessionId: async () => {}, + sessionId, }); - return { context, events, runId, sseFrameBytes, translator }; + return { context, events, runId, sessionId, sseFrameBytes, translator }; } describe("Claude Agent SDK durable event boundaries", () => { @@ -354,6 +354,7 @@ describe("Claude Agent SDK durable event boundaries", () => { publisher.pushTerminal(pushContext, reason, closures, terminal), recordNativeSessionId: async () => {}, replaceNativeSessionId: async () => {}, + sessionId: context.payload.execution.run.sessionId, }); let failure: ClaudeDurableEventTooLargeError | null = null; @@ -431,6 +432,7 @@ describe("Claude Agent SDK durable event boundaries", () => { publisher.pushTerminal(pushContext, reason, closures, terminal), recordNativeSessionId: async () => {}, replaceNativeSessionId: async () => {}, + sessionId: context.payload.execution.run.sessionId, }); await expect( @@ -466,10 +468,9 @@ describe("Claude Agent SDK durable event boundaries", () => { test("maps oversized native tool IDs and rejects oversized tool names before durable state", async () => { const accepted = createCmaHarness(); + const replayed = createCmaHarness({ runId: accepted.runId, sessionId: accepted.sessionId }); const nativeToolCallId = `tool-${"x".repeat(1_100_000)}`; - - await accepted.translator.handleSdkMessage( - accepted.context, + const frames = [ { message: { content: [{ id: nativeToolCallId, input: {}, name: "Read", type: "tool_use" }], @@ -477,11 +478,7 @@ describe("Claude Agent SDK durable event boundaries", () => { }, type: "assistant", uuid: "wire-long-tool", - } as unknown as SDKMessage, - accepted.runId, - ); - await accepted.translator.handleSdkMessage( - accepted.context, + }, { decision_reason: "Blocked by policy", message: "Denied by policy", @@ -490,11 +487,7 @@ describe("Claude Agent SDK durable event boundaries", () => { tool_use_id: nativeToolCallId, type: "system", uuid: "wire-long-tool-advisory", - } as unknown as SDKMessage, - accepted.runId, - ); - await accepted.translator.handleSdkMessage( - accepted.context, + }, { is_error: false, modelUsage: {}, @@ -505,9 +498,14 @@ describe("Claude Agent SDK durable event boundaries", () => { type: "result", usage: {}, uuid: "wire-long-tool-result", - } as unknown as SDKMessage, - accepted.runId, - ); + }, + ] as unknown as SDKMessage[]; + + for (const harness of [accepted, replayed]) { + for (const frame of frames) { + await harness.translator.handleSdkMessage(harness.context, frame, harness.runId); + } + } const toolEvents = accepted.events.filter( (event) => event.kind === "item.started" || event.kind === "tool.call.updated", @@ -527,6 +525,7 @@ describe("Claude Agent SDK durable event boundaries", () => { ).toBe(true); expect(JSON.stringify(accepted.events)).not.toContain(nativeToolCallId); expect(accepted.sseFrameBytes.every((bytes) => bytes < CMA_MAX_EVENT_BYTES)).toBe(true); + expect(replayed.events).toEqual(accepted.events); const rejected = createCmaHarness(); const oversizedName = "n".repeat(1_100_000); @@ -644,6 +643,7 @@ describe("Claude Agent SDK durable event boundaries", () => { pushTerminal: async () => {}, recordNativeSessionId: async () => {}, replaceNativeSessionId: async () => {}, + sessionId: context.payload.execution.run.sessionId, }); await translator.handleSdkMessage( diff --git a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts index 8c6207d..0150819 100644 --- a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts @@ -505,10 +505,12 @@ describe("Claude Agent SDK provider fixtures", () => { test("materializes authoritative result permission denials without a tool result", async () => { const { context, events, translator } = createHarness(); + const nativeAgentId = `agent-${"a".repeat(300)}`; await translator.handleSdkMessage( context, { + agent_id: nativeAgentId, decision_reason: "Blocked by policy X", decision_reason_type: "rule", message: "Denied by policy X", @@ -552,6 +554,16 @@ describe("Claude Agent SDK provider fixtures", () => { }), }), ); + const denial = events().findLast( + (event) => + event.kind === "tool.call.updated" && + isRecord(event.payload) && + event.payload["toolCallId"] === "tool-denied", + ); + const agentId = + denial !== undefined && isRecord(denial.payload) ? denial.payload["agentId"] : null; + expect(agentId).toMatch(/^rid1_[A-Za-z0-9_-]{43}$/); + expect(agentId).not.toBe(nativeAgentId); expect(events().filter(({ kind }) => kind === "item.started")).toHaveLength(1); expect(events().filter(({ kind }) => kind === "item.completed")).toHaveLength(1); }); diff --git a/tests/claude-agent-sdk-query-options.test.ts b/tests/claude-agent-sdk-query-options.test.ts index ddcdc46..24ec3c9 100644 --- a/tests/claude-agent-sdk-query-options.test.ts +++ b/tests/claude-agent-sdk-query-options.test.ts @@ -221,11 +221,12 @@ describe("Claude Agent SDK query options", () => { ); const abortController = new AbortController(); + const nativeAgentId = `subagent-${"a".repeat(300)}`; const result = options.canUseTool?.( "Bash", { command: "pwd" }, { - agentID: "subagent-1", + agentID: nativeAgentId, blockedPath: "/workspace/secret", decisionReason: "Path is outside the allowed roots.", description: "Read access to /workspace/secret", @@ -253,7 +254,6 @@ describe("Claude Agent SDK query options", () => { await drainClaudeTasks(permissionTasks); expect(permissionTasks.size).toBe(0); expect(permissionInput).toMatchObject({ - agentId: "subagent-1", blockedPath: "/workspace/secret", decisionReason: "Path is outside the allowed roots.", description: "Read access to /workspace/secret", @@ -265,6 +265,9 @@ describe("Claude Agent SDK query options", () => { requestId: "permission-request-1", toolCallId: "tool-1", }); + const publicAgentId = (permissionInput as { agentId?: string } | null)?.agentId; + expect(publicAgentId).toMatch(/^rid1_[A-Za-z0-9_-]{43}$/); + expect(publicAgentId).not.toBe(nativeAgentId); expect(permissionSignal).toBe(abortController.signal); }); diff --git a/tests/claude-agent-sdk-task-events.test.ts b/tests/claude-agent-sdk-task-events.test.ts index bd1b880..2e27569 100644 --- a/tests/claude-agent-sdk-task-events.test.ts +++ b/tests/claude-agent-sdk-task-events.test.ts @@ -85,7 +85,7 @@ describe("Claude Agent SDK task snapshots", () => { payload: { tasks: [ { - taskId: expect.stringMatching(/^claude-task:[a-f0-9]{64}$/), + taskId: expect.stringMatching(/^rid1_[A-Za-z0-9_-]{43}$/), title: expect.stringMatching(/^x{4095}$/), }, { taskId: "empty-metadata" }, diff --git a/tests/claude-agent-sdk-test-helpers.ts b/tests/claude-agent-sdk-test-helpers.ts index e4701ba..13c4551 100644 --- a/tests/claude-agent-sdk-test-helpers.ts +++ b/tests/claude-agent-sdk-test-helpers.ts @@ -38,6 +38,7 @@ export function createClaudeAgentSdkHarness( replaceNativeSessionId: async (_context, previousSessionId, nextSessionId) => { nativeSessionResets.push([previousSessionId, nextSessionId]); }, + sessionId: context.payload.execution.run.sessionId, }); return { diff --git a/tests/fixtures/providers/openai-app-server/cases/agent-message-completed.json b/tests/fixtures/providers/openai-app-server/cases/agent-message-completed.json index f157dca..70d9f6a 100644 --- a/tests/fixtures/providers/openai-app-server/cases/agent-message-completed.json +++ b/tests/fixtures/providers/openai-app-server/cases/agent-message-completed.json @@ -31,7 +31,7 @@ "messageId": "", "role": "agent" }, - "sourceEventId": "openai.item.completed:turn-1:message-1:0" + "sourceEventId": "openai.item.completed:sid1_BN1wwFn-vJ2yK8Ax93DBy3pMPeQlPs_fMiD3xICce38:0" }, { "kind": "message.completed", @@ -39,7 +39,7 @@ "messageId": "", "role": "agent" }, - "sourceEventId": "openai.item.completed:turn-1:message-1:1" + "sourceEventId": "openai.derived:sid1_BARJThUDVPe3fUNTSKJ0PBt0smINLtwyX9wLH2fXPTk" } ] } diff --git a/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json b/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json index 1de650d..e0f3d17 100644 --- a/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json +++ b/tests/fixtures/providers/openai-app-server/cases/command-output-stream.json @@ -52,7 +52,7 @@ "parentMessageId": "", "title": "Shell" }, - "sourceEventId": "openai.item.started:turn-1:cmd-1" + "sourceEventId": "openai.item.started:sid1_diOiKEZok2HcGrWKr19uxht3PPOLSj_ZVMcjnOYcZYo" }, { "kind": "tool.call.updated", @@ -63,7 +63,7 @@ "title": "Shell", "toolCallId": "cmd-1" }, - "sourceEventId": "openai.tool.started:turn-1:cmd-1" + "sourceEventId": "openai.tool.started:sid1_RWKjM-yo_eO1O7FFfDWAa7yqJPtBWMGIYPSniQQkjHs" }, { "delivery": "best_effort", diff --git a/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json b/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json index 73b7b2a..2c83941 100644 --- a/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json +++ b/tests/fixtures/providers/openai-app-server/cases/error-before-tracked-turn.json @@ -51,7 +51,7 @@ "delivery": "lossless", "kind": "agent.tasks.replaced", "payload": { "tasks": [] }, - "sourceEventId": "openai.turn.failed.closure:turn-1:0", + "sourceEventId": "openai.derived:sid1_mw1rK-IfsZaez6IOgvoBLwbJVWO3Ou-eANcCdudVblc", "visibility": "participant" }, { diff --git a/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json b/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json index 23cbe96..0a07e74 100644 --- a/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json +++ b/tests/fixtures/providers/openai-app-server/cases/root-with-parallel-child-turns.json @@ -191,7 +191,7 @@ "parentMessageId": "", "title": "Spawn agent" }, - "sourceEventId": "openai.item.started:turn-root:agent-call-1" + "sourceEventId": "openai.item.started:sid1_ZbP7VMPfxU99NO5_Ssl7dpRsfsJCg5iDDTS3opi8UyQ" }, { "kind": "tool.call.updated", @@ -202,7 +202,7 @@ "title": "Spawn agent", "toolCallId": "agent-call-1" }, - "sourceEventId": "openai.tool.started:turn-root:agent-call-1" + "sourceEventId": "openai.tool.started:sid1_x5P0XypwtBPbQ87N6EdslwKLXsWaZ2Wsw5cgxVjLkKQ" }, { "kind": "tool.call.updated", @@ -223,7 +223,7 @@ }, "toolCallId": "agent-call-1" }, - "sourceEventId": "openai.item.completed:turn-root:agent-call-1:0" + "sourceEventId": "openai.item.completed:sid1_C8cwMXkrQNCQEgerqw8tRi-Enh7ZKfj_DA9i9vH-rhU:0" }, { "kind": "item.completed", @@ -232,22 +232,27 @@ "itemType": "tool_call", "status": "completed" }, - "sourceEventId": "openai.item.completed:turn-root:agent-call-1:1" + "sourceEventId": "openai.derived:sid1_5GlNQBVpErPuMjUZcwLG6BitzcN1MSXZRXotq6VljPc" + }, + { + "kind": "message.started", + "payload": { "messageId": "", "role": "agent" }, + "sourceEventId": "openai.message.started:" }, { "delivery": "lossless", "kind": "message.added", "payload": { "content": "PARENT_OK: CHILD_A + CHILD_B", - "messageId": "", + "messageId": "", "role": "agent" }, - "sourceEventId": "openai.item.completed:turn-root:message-parent:0" + "sourceEventId": "openai.item.completed:sid1_FjjaUjXqCeULq2o7LJ4Ia93QFJqz2B5kI5iVqKiW9uI:0" }, { "kind": "message.completed", - "payload": { "messageId": "", "role": "agent" }, - "sourceEventId": "openai.item.completed:turn-root:message-parent:1" + "payload": { "messageId": "", "role": "agent" }, + "sourceEventId": "openai.derived:sid1_hw9aor_2uReuaZl1eJIaXyugelmcCDTogRNGzqyELbQ" }, { "kind": "runtime.resume.updated", @@ -259,9 +264,15 @@ "kind": "agent.tasks.replaced", "payload": { "tasks": [] }, "runId": "", - "sourceEventId": "openai.turn.completed.closure:turn-root:0", + "sourceEventId": "openai.derived:sid1_r5WSt7u4mCiLVpw75XQhDXCCtYBUw-StSBWHAbAdolk", "visibility": "participant" }, + { + "kind": "message.completed", + "payload": { "messageId": "", "role": "agent" }, + "runId": "", + "sourceEventId": "openai.derived:sid1_urrGMu3mQ34ngjJJU5uUqSjpIbhmCWsyIKYxjFeCmf4" + }, { "kind": "run.completed", "native": { @@ -270,7 +281,7 @@ "turnId": "turn-root" }, "payload": { - "finalMessageId": "", + "finalMessageId": "", "stopReason": "end_turn" }, "runId": "", diff --git a/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json b/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json index f2f2059..a51b508 100644 --- a/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json +++ b/tests/fixtures/providers/openai-app-server/cases/turn-completed-with-final-agent-message.json @@ -51,7 +51,7 @@ "messageId": "", "role": "agent" }, - "sourceEventId": "openai.item.completed:turn-1:message-1:0" + "sourceEventId": "openai.item.completed:sid1_BN1wwFn-vJ2yK8Ax93DBy3pMPeQlPs_fMiD3xICce38:0" }, { "kind": "message.completed", @@ -59,7 +59,7 @@ "messageId": "", "role": "agent" }, - "sourceEventId": "openai.item.completed:turn-1:message-1:1" + "sourceEventId": "openai.derived:sid1_BARJThUDVPe3fUNTSKJ0PBt0smINLtwyX9wLH2fXPTk" }, { "kind": "runtime.resume.updated", @@ -73,7 +73,7 @@ "delivery": "lossless", "kind": "agent.tasks.replaced", "payload": { "tasks": [] }, - "sourceEventId": "openai.turn.completed.closure:turn-1:0", + "sourceEventId": "openai.derived:sid1_65gCZZFCtIH2m10-eUUIasKg5wmDWEVoaKGA1D26hMk", "visibility": "participant" }, { diff --git a/tests/fixtures/providers/openai-app-server/cases/turn-plan-updated.json b/tests/fixtures/providers/openai-app-server/cases/turn-plan-updated.json index 1bbc112..2a9c578 100644 --- a/tests/fixtures/providers/openai-app-server/cases/turn-plan-updated.json +++ b/tests/fixtures/providers/openai-app-server/cases/turn-plan-updated.json @@ -36,8 +36,7 @@ } ], "source": "driver" - }, - "sourceEventId": "openai.turn.plan:turn-1:0" + } } ] } diff --git a/tests/openai-app-server-agent-task-events.test.ts b/tests/openai-app-server-agent-task-events.test.ts index 88b96a4..37d6ef5 100644 --- a/tests/openai-app-server-agent-task-events.test.ts +++ b/tests/openai-app-server-agent-task-events.test.ts @@ -185,7 +185,7 @@ describe("OpenAI app-server agent task snapshots", () => { const replay = tasks( apply(state, { agentId: longId, agentPath: "/root/replayed", kind: "started" }), ); - expect(first[0]).toMatchObject({ taskId: expect.stringMatching(/^openai-task:[\da-f]{64}$/) }); + expect(first[0]).toMatchObject({ taskId: expect.stringMatching(/^rid1_[A-Za-z0-9_-]{43}$/) }); expect(replay[0]).toMatchObject({ taskId: (first[0] as { taskId: string }).taskId }); expect((first[0] as { title: string }).title).toHaveLength(4_095); expect(openAiAgentTasksClosedEvent()).toEqual({ diff --git a/tests/openai-app-server-client.test.ts b/tests/openai-app-server-client.test.ts index d4149f1..99a2394 100644 --- a/tests/openai-app-server-client.test.ts +++ b/tests/openai-app-server-client.test.ts @@ -1229,7 +1229,7 @@ setInterval(() => {}, 1000); permissionAborted.resolve(); await permissionGate.promise; throw new PermissionEventDeliveryError( - "item/commandExecution/requestApproval:41", + "item/commandExecution/requestApproval:number:41", "resolved", new Error("event sink unavailable"), ); diff --git a/tests/openai-app-server-event-bridge-items.test.ts b/tests/openai-app-server-event-bridge-items.test.ts index cc0e76a..300d3a1 100644 --- a/tests/openai-app-server-event-bridge-items.test.ts +++ b/tests/openai-app-server-event-bridge-items.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; +import type { DriverEventInput } from "../src/protocol/events"; import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; -import { OpenAiPublicIdState } from "../src/runtimes/openai/app-server-event-state"; import { parseServerNotification } from "../src/runtimes/openai/app-server-protocol-server"; import { OpenAiTurnTracker } from "../src/runtimes/openai/app-server-turn-tracker"; import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; @@ -28,17 +28,156 @@ async function handleOfficialNotification( } describe("OpenAi app-server event bridge", () => { - test("keeps bounded provider identities stable and namespace distinct", () => { - const ids = new OpenAiPublicIdState(); + test("keeps bounded provider identities stable across lifecycle boundaries", () => { + const { bridge } = createHarness(); const nativeId = "native".repeat(100); - const publicItemId = ids.publicId(nativeId, "item"); - const publicTurnId = ids.publicId(nativeId, "turn"); + const publicItemId = bridge.mapToolCallId(nativeId); + const publicTurnId = bridge.publicTurnId(nativeId); - expect(ids.publicId(nativeId, "item")).toBe(publicItemId); - expect(ids.publicId(nativeId, "turn")).toBe(publicTurnId); + bridge.clearActiveTurns(); + + for (const candidate of [bridge, createHarness().bridge]) { + expect(candidate.mapToolCallId(nativeId)).toBe(publicItemId); + expect(candidate.publicTurnId(nativeId)).toBe(publicTurnId); + } expect(publicItemId).not.toBe(publicTurnId); + expect(publicItemId).toBe("rid1_KnwXRN9srTgnCleHSt-EmI8jC7h9g2foozL7GxrsP9o"); expect(publicItemId.length).toBeLessThan(nativeId.length); expect(publicTurnId.length).toBeLessThan(nativeId.length); + expect(bridge.mapToolCallId(`rid1_${"x".repeat(43)}`)).not.toBe(`rid1_${"x".repeat(43)}`); + expect(bridge.mapToolCallId("\ud800".repeat(86))).not.toBe( + bridge.mapToolCallId("\ud801".repeat(86)), + ); + }); + + test.each([ + [ + "agent message", + async (harness: ReturnType, itemId: string, turnId: string) => + harness.bridge.handleNotification(harness.context, "item/completed", { + item: { id: itemId, text: "stable reply", type: "agentMessage" }, + threadId: "thread-1", + turnId, + }), + ], + [ + "reasoning", + async (harness: ReturnType, itemId: string, turnId: string) => + harness.bridge.handleNotification(harness.context, "item/reasoning/summaryTextDelta", { + delta: "stable thought", + itemId, + summaryIndex: 0, + threadId: "thread-1", + turnId, + }), + ], + [ + "tool", + async (harness: ReturnType, itemId: string, turnId: string) => + harness.bridge.handleNotification(harness.context, "item/started", { + item: { id: itemId, status: "inProgress", type: "commandExecution" }, + threadId: "thread-1", + turnId, + }), + ], + ] as const)("keeps complete oversized %s replay events stable", async (_kind, replay) => { + const itemId = `item-${"i".repeat(300)}`; + const turnId = `turn-${"t".repeat(300)}`; + const attempts: DriverEventInput[][] = []; + + for (let generation = 0; generation < 2; generation += 1) { + const harness = createHarness(); + await replay(harness, itemId, turnId); + attempts.push(harness.events()); + } + + expect(attempts[0]?.length).toBeGreaterThan(0); + expect(attempts[1]).toEqual(attempts[0]); + }); + + test("keeps item messages and synthetic tool parents independent of notification order", async () => { + const project = async (order: readonly ("message" | "tool")[]) => { + const harness = createHarness(); + + for (const kind of order) { + if (kind === "message") { + await harness.bridge.handleNotification(harness.context, "item/completed", { + item: { id: "message-1", text: "reply", type: "agentMessage" }, + threadId: "thread-1", + turnId: "turn-1", + }); + } else { + await harness.bridge.handleNotification(harness.context, "item/started", { + item: { id: "tool-1", status: "inProgress", type: "commandExecution" }, + threadId: "thread-1", + turnId: "turn-1", + }); + } + } + + const message = harness.events().find((event) => event.kind === "message.added"); + const tool = harness + .events() + .find( + (event) => + event.kind === "item.started" && + readEventPayloadString(event, "itemType") === "tool_call", + ); + return { + messageId: message === undefined ? null : readEventPayloadString(message, "messageId"), + toolParentId: tool === undefined ? null : readEventPayloadString(tool, "parentMessageId"), + }; + }; + + const messageFirst = await project(["message", "tool"]); + const toolFirst = await project(["tool", "message"]); + + expect(toolFirst).toEqual(messageFirst); + expect(toolFirst.messageId).not.toBe(toolFirst.toolParentId); + }); + + test("separates provider tuples and lone-surrogate identities", async () => { + const identities: Array = []; + + for (const [turnId, itemId] of [ + ["a", "b:c"], + ["a:b", "c"], + ["unicode", "\ud800".repeat(86)], + ["unicode", "\ud801".repeat(86)], + ]) { + const harness = createHarness(); + await harness.bridge.handleNotification(harness.context, "item/completed", { + item: { id: itemId, text: "reply", type: "agentMessage" }, + threadId: "thread-1", + turnId, + }); + const started = harness.events().find((event) => event.kind === "message.started"); + const added = harness.events().find((event) => event.kind === "message.added"); + const toolHarness = createHarness(); + await toolHarness.bridge.handleNotification(toolHarness.context, "item/started", { + item: { id: itemId, status: "inProgress", type: "commandExecution" }, + threadId: "thread-1", + turnId, + }); + identities.push([ + started === undefined ? null : readEventPayloadString(started, "messageId"), + added?.sourceEventId, + toolHarness.events().find((event) => event.kind === "item.started")?.sourceEventId, + ]); + } + + expect(new Set(identities.map(([messageId]) => messageId)).size).toBe(identities.length); + expect(new Set(identities.map(([, sourceEventId]) => sourceEventId)).size).toBe( + identities.length, + ); + expect(new Set(identities.map(([, , sourceEventId]) => sourceEventId)).size).toBe( + identities.length, + ); + expect(identities[0]).toEqual([ + "10DF94GHQDTF5928TMBEZSWHQ4", + "openai.item.completed:sid1_U4oWh4UGZEw808sI5zy0_aOCd0-dwfUwV07Mia3XjNA:0", + "openai.item.started:sid1_0MMp18iqK51t_lCY7b8CZMB4CuwCFtDacjitEs2EkVE", + ]); }); test("bounds terminal turn deduplication and clears it on shutdown", () => { @@ -181,7 +320,7 @@ describe("OpenAi app-server event bridge", () => { taskId: "agent-1", title: "Sub-agent completed", }, - sourceEventId: "openai.item.completed:turn-1:activity-1:0", + sourceEventId: "openai.derived:sid1_kECA0eVWvzfAvIr0LN7vWra6VGTB8rTQoAgf7EHr7Rg", }, ]); @@ -432,12 +571,60 @@ describe("OpenAi app-server event bridge", () => { const taskUpdate = harness.events().find((event) => event.kind === "agent.task.updated"); const taskSnapshot = harness.events().find((event) => event.kind === "agent.tasks.replaced"); const taskId = taskUpdate === undefined ? null : readEventPayloadString(taskUpdate, "taskId"); - expect(taskId).toMatch(/^openai-task:[\da-f]{64}$/); + expect(taskId).toMatch(/^rid1_[A-Za-z0-9_-]{43}$/); + expect(taskUpdate === undefined ? null : readEventPayloadString(taskUpdate, "agentId")).toBe( + taskId, + ); expect(taskSnapshot).toMatchObject({ payload: { tasks: [{ taskId }] }, }); }); + test("bounds collaboration thread identities consistently", async () => { + const harness = createHarness(); + const nativeThreadId = `agent-${"a".repeat(300)}`; + + await harness.bridge.handleNotification(harness.context, "item/completed", { + item: { + agentsStates: { [nativeThreadId]: { message: "done", status: "completed" } }, + id: "collab-1", + model: null, + prompt: null, + reasoningEffort: null, + receiverThreadIds: [nativeThreadId], + senderThreadId: nativeThreadId, + status: "completed", + tool: "wait", + type: "collabAgentToolCall", + }, + threadId: "thread-1", + turnId: "turn-1", + }); + + const terminal = harness + .events() + .find( + (event) => + event.kind === "tool.call.updated" && + readEventPayloadString(event, "status") === "completed", + ); + const agentId = terminal === undefined ? null : readEventPayloadString(terminal, "agentId"); + const structuredOutput = + terminal?.payload !== null && + typeof terminal?.payload === "object" && + !Array.isArray(terminal.payload) + ? (terminal.payload as Record)["structuredOutput"] + : null; + + expect(agentId).toMatch(/^rid1_[A-Za-z0-9_-]{43}$/); + expect(JSON.stringify(structuredOutput)).not.toContain(nativeThreadId); + expect(structuredOutput).toMatchObject({ + agentsStates: { [agentId!]: { message: "done", status: "completed" } }, + receiverThreadIds: [agentId], + senderThreadId: agentId, + }); + }); + test("replays a plan delta after delivery rejection without duplicating content", async () => { const harness = createHarness({ failReasonOnce: "driver.openai.plan.delta" }); const notification = { @@ -461,19 +648,51 @@ describe("OpenAi app-server event bridge", () => { expect(accepted.at(-1)?.events[0]?.payload).toMatchObject({ entries: [{ content: "step two", status: "in_progress" }], }); - expect(accepted.map((batch) => batch.events[0]?.sourceEventId)).toEqual([ - "openai.plan.delta:turn-1:plan-1:0", - "openai.plan.delta:turn-1:plan-1:4", - ]); - expect( - harness.attempts - .filter((batch) => batch.reason === "driver.openai.plan.delta") - .slice(0, 2) - .map((batch) => batch.events[0]?.sourceEventId), - ).toEqual(["openai.plan.delta:turn-1:plan-1:0", "openai.plan.delta:turn-1:plan-1:0"]); + expect(accepted.every((batch) => batch.events[0]?.sourceEventId === undefined)).toBe(true); expect(JSON.stringify(accepted)).not.toContain("stepstep"); }); + test("does not invent cross-process occurrence IDs for unsequenced provider updates", async () => { + const planHarness = createHarness(); + for (const step of ["A", "B", "A"]) { + await planHarness.bridge.handleNotification(planHarness.context, "turn/plan/updated", { + explanation: null, + plan: [{ status: "inProgress", step }], + threadId: "thread-1", + turnId: "turn-1", + }); + } + const planEvents = planHarness.events().filter((event) => event.kind === "plan.updated"); + expect(planEvents.map((event) => event.sourceEventId)).toEqual([ + undefined, + undefined, + undefined, + ]); + + const reasoningHarness = createHarness(); + for (const delta of ["same", "same"]) { + await reasoningHarness.bridge.handleNotification( + reasoningHarness.context, + "item/reasoning/summaryTextDelta", + { + delta, + itemId: "reasoning-1", + summaryIndex: 0, + threadId: "thread-1", + turnId: "turn-1", + }, + ); + } + const reasoningDeltas = reasoningHarness + .events() + .filter((event) => event.kind === "thought.delta"); + expect(reasoningDeltas.map((event) => event.sourceEventId)).toEqual([undefined, undefined]); + expect(reasoningDeltas.map((event) => readEventPayloadString(event, "contentDelta"))).toEqual([ + "same", + "same", + ]); + }); + test("retries message start with the same identity after delivery rejection", async () => { const harness = createHarness({ failReasonOnce: "driver.openai.message.started" }); const notification = { diff --git a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts index 3861830..86acd0a 100644 --- a/tests/openai-app-server-event-bridge-run-lifecycle.test.ts +++ b/tests/openai-app-server-event-bridge-run-lifecycle.test.ts @@ -344,7 +344,7 @@ describe("OpenAi app-server event bridge", () => { ]); }); - test("rotates assistant message identity after each completed agent item", async () => { + test("isolates assistant items from the turn-scoped tool parent", async () => { const { bridge, context, events } = createHarness(); const progressMessages = [ @@ -450,8 +450,10 @@ describe("OpenAi app-server event bridge", () => { .map((event) => readEventPayloadString(event, "parentMessageId")) .find((messageId): messageId is string => messageId !== null); - expect(toolParentMessageId).toBe(assistantMessages.at(-1)?.messageId); - expect(events().filter((event) => event.kind === "message.completed")).toHaveLength(4); + expect(assistantMessages.some(({ messageId }) => messageId === toolParentMessageId)).toBe( + false, + ); + expect(events().filter((event) => event.kind === "message.completed")).toHaveLength(5); expect(events().filter((event) => event.kind === "run.completed")).toHaveLength(1); }); @@ -672,7 +674,6 @@ describe("OpenAi app-server event bridge", () => { ], source: "driver", }, - sourceEventId: "openai.turn.plan:turn-1:0", }, { kind: "plan.updated", @@ -691,7 +692,6 @@ describe("OpenAi app-server event bridge", () => { ], source: "driver", }, - sourceEventId: "openai.turn.plan:turn-1:1", }, ]); }); diff --git a/tests/openai-app-server-event-bridge-terminal.test.ts b/tests/openai-app-server-event-bridge-terminal.test.ts index 916b225..59110db 100644 --- a/tests/openai-app-server-event-bridge-terminal.test.ts +++ b/tests/openai-app-server-event-bridge-terminal.test.ts @@ -9,6 +9,7 @@ import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; +import { createRuntimeSourceEventId } from "../src/runtimes/runtime-public-id"; import { parseServerNotification, parseServerRequest, @@ -663,7 +664,8 @@ describe("OpenAi app-server event bridge", () => { event.kind === "diagnostic.reported" && readEventPayloadString(event, "message") === "OpenAI turn diff updated.", ); - expect(isDriverId(publicItemId)).toBe(true); + expect(publicItemId).not.toBe(nativeItemId); + expect(Buffer.byteLength(publicItemId, "utf8")).toBeLessThanOrEqual(256); expect(permissionInput.toolCallId).toBe(publicItemId); expect(readEventPayloadString(review!, "targetItemId")).toBe(publicItemId); expect(progress?.payload).toMatchObject({ @@ -678,8 +680,8 @@ describe("OpenAi app-server event bridge", () => { status: "running", }); expect(shell?.payload).not.toHaveProperty("processId"); - expect(isDriverId(readEventPayloadString(shell!, "threadId"))).toBe(true); - expect(isDriverId(readEventPayloadString(shell!, "turnId"))).toBe(true); + expect(readEventPayloadString(shell!, "threadId")).not.toBe(nativeThreadId); + expect(readEventPayloadString(shell!, "turnId")).not.toBe(nativeTurnId); expect(readEventPayloadString(diff!, "turnId")).toBe(readEventPayloadString(shell!, "turnId")); expect(delivered.every((event) => Buffer.byteLength(JSON.stringify(event)) < 1_048_576)).toBe( true, @@ -797,7 +799,8 @@ describe("OpenAi app-server event bridge", () => { }); test("removes OpenAI private citation markup from streamed and final assistant text", async () => { - const { bridge, context, events } = createHarness(); + const { bridge, context, delivered } = createPublisherHarness(); + const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); const privateCitation = "\uE200cite\uE202turn7search12\uE202turn8view0\uE201"; await bridge.handleNotification(context, "item/agentMessage/delta", { @@ -841,8 +844,9 @@ describe("OpenAi app-server event bridge", () => { status: "completed", }, }); + await expect(trackedTurn).resolves.toBeUndefined(); - const allEvents = events(); + const allEvents = delivered; const streamedText = allEvents .filter((event) => event.kind === "message.delta") .map((event) => readEventPayloadString(event, "contentDelta") ?? "") @@ -858,6 +862,7 @@ describe("OpenAi app-server event bridge", () => { expect(runCompleted).toBeDefined(); expectRunFinalReferences(allEvents, "beforeafter"); expect(diagnostics).toHaveLength(1); + expect(new Set(allEvents.map((event) => event.sourceEventId)).size).toBe(allEvents.length); }); test("flushes incomplete private markup when an item completes without a text snapshot", async () => { @@ -1149,7 +1154,11 @@ describe("OpenAi app-server event bridge", () => { }); test("commits a chunked snapshot once after a partial transport retry", async () => { - const sourcePrefix = "openai.item.completed:turn-1:message-partial:"; + const sourcePrefix = `${createRuntimeSourceEventId( + "openai.item.completed", + "turn-1", + "message-partial", + )}:`; const { bridge, context, delivered, partialAttempts } = createPublisherHarness({ partialSourcePrefix: sourcePrefix, }); @@ -1184,13 +1193,7 @@ describe("OpenAi app-server event bridge", () => { .filter((event) => event.sourceEventId?.startsWith(sourcePrefix) === true) .map((event) => event.sourceEventId); expect(new Set(deliveredSnapshotIds).size).toBe(deliveredSnapshotIds.length); - expect( - delivered.filter( - (event) => - event.kind === "message.completed" && - event.sourceEventId?.startsWith(sourcePrefix) === true, - ), - ).toHaveLength(1); + expect(delivered.filter((event) => event.kind === "message.completed")).toHaveLength(1); expectRunFinalReferences(delivered, finalText); expect(delivered.at(-1)?.kind).toBe("run.completed"); }); @@ -1504,7 +1507,12 @@ describe("OpenAi app-server event bridge", () => { const reasoningEvents = delivered.filter((event) => event.kind.startsWith("thought.")); const thoughtIds = reasoningEvents.map((event) => readEventPayloadString(event, "thoughtId")); - expect(thoughtIds.every((thoughtId) => thoughtId !== null && isDriverId(thoughtId))).toBe(true); + expect( + thoughtIds.every( + (thoughtId) => + thoughtId !== null && thoughtId !== itemId && Buffer.byteLength(thoughtId, "utf8") <= 256, + ), + ).toBe(true); expect(new Set(thoughtIds).size).toBe(1); expect( reasoningEvents @@ -1584,8 +1592,17 @@ describe("OpenAi app-server event bridge", () => { (event) => readEventPayloadString(event, "toolCallId") ?? readEventPayloadString(event, "itemId"), ); - expect(compactedId !== null && isDriverId(compactedId)).toBe(true); - expect(toolIds.every((toolId) => toolId !== null && isDriverId(toolId))).toBe(true); + expect( + compactedId !== null && + compactedId !== contextItemId && + Buffer.byteLength(compactedId, "utf8") <= 256, + ).toBe(true); + expect( + toolIds.every( + (toolId) => + toolId !== null && toolId !== toolItemId && Buffer.byteLength(toolId, "utf8") <= 256, + ), + ).toBe(true); expect(new Set(toolIds).size).toBe(1); expect( delivered.some( diff --git a/tests/openai-app-server-provider-fixtures.test.ts b/tests/openai-app-server-provider-fixtures.test.ts index f75a5f2..ef7c147 100644 --- a/tests/openai-app-server-provider-fixtures.test.ts +++ b/tests/openai-app-server-provider-fixtures.test.ts @@ -1126,7 +1126,6 @@ describe("OpenAI app-server provider fixtures", () => { bridge, context, ); - expect(events()).toEqual([ { delivery: "lossless", @@ -1146,7 +1145,7 @@ describe("OpenAI app-server provider fixtures", () => { taskId: "agent-1", title: `Sub-agent ${activityKind}`, }, - sourceEventId: `openai.item.completed:turn-1:activity-${activityKind}:0`, + sourceEventId: expect.stringMatching(/^openai\.derived:sid1_/), }, { delivery: "lossless", @@ -1163,7 +1162,7 @@ describe("OpenAI app-server provider fixtures", () => { ] : [], }, - sourceEventId: `openai.item.completed:turn-1:activity-${activityKind}:1`, + sourceEventId: expect.stringMatching(/^openai\.derived:sid1_/), visibility: "participant", }, ]); @@ -1214,7 +1213,7 @@ describe("OpenAI app-server provider fixtures", () => { taskId: "agent-replayed", title: "Sub-agent interacted", }, - sourceEventId: "openai.item.completed:turn-snapshot:activity-replayed:0", + sourceEventId: expect.stringMatching(/^openai\.derived:sid1_/), }); const snapshots = events().filter((event) => event.kind === "agent.tasks.replaced"); expect(snapshots).toMatchObject([ diff --git a/tests/openai-app-server-request-handler.test.ts b/tests/openai-app-server-request-handler.test.ts index e4d7684..f407273 100644 --- a/tests/openai-app-server-request-handler.test.ts +++ b/tests/openai-app-server-request-handler.test.ts @@ -26,7 +26,7 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as const requestSignal = signal ?? new AbortController().signal; permissionInputs.push(input); permissionSignals.push(requestSignal); - if (permissionInputs.length === 3) { + if (permissionInputs.length === 4) { permissionStarted.resolve(); } if (input.toolKind === "item/permissions/requestApproval") { @@ -77,6 +77,10 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as ], itemId: "tool-1", }); + handler.dispatch("item/commandExecution/requestApproval", "3", { + command: "npm test", + itemId: "tool-string-3", + }); handler.dispatch("item/fileChange/requestApproval", 4, { grantRoot: "/workspace", itemId: "tool-2", @@ -95,6 +99,7 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as expect.arrayContaining([ { id: 1, result: { currentTimeAt: expect.any(Number) } }, { id: 3, result: { decision: "cancel" } }, + { id: "3", result: { decision: "accept" } }, { id: 5, result: { @@ -119,6 +124,7 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as command: "npm test", cwd: "/workspace", }), + expect.objectContaining({ command: "npm test", itemId: "tool-string-3" }), expect.objectContaining({ grantRoot: "/workspace", reason: "Apply changes" }), expect.objectContaining({ cwd: "/workspace", @@ -127,10 +133,17 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as }), ]); expect(permissionInputs.map(({ title }) => title)).toEqual([ + "Approve command execution", "Approve command execution", "Approve file changes", "Approve runtime permissions", ]); + expect(permissionInputs.map(({ requestId }) => requestId)).toEqual([ + "item/commandExecution/requestApproval:number:3", + "item/commandExecution/requestApproval:string:3", + "item/fileChange/requestApproval:number:4", + "item/permissions/requestApproval:number:5", + ]); expect(rejections.some((response) => response.id === 3)).toBe(false); expect(errors).toEqual([]); } finally { @@ -181,7 +194,7 @@ test("OpenAI server request cancellation propagates permission delivery failures const permissionStarted = Promise.withResolvers(); const deliveryGate = Promise.withResolvers(); const deliveryError = new PermissionEventDeliveryError( - "item/commandExecution/requestApproval:3", + "item/commandExecution/requestApproval:number:3", "resolved", new Error("event sink unavailable"), ); diff --git a/tests/openai-app-server-startup.test.ts b/tests/openai-app-server-startup.test.ts index b2b3a84..c7a02be 100644 --- a/tests/openai-app-server-startup.test.ts +++ b/tests/openai-app-server-startup.test.ts @@ -479,7 +479,11 @@ process.stdin.on("data", (chunk) => { } if ( holdTurnTiming && - input.events.some((event) => event.sourceEventId === "openai.provider.turn_start:turn-1") + input.events.some( + (event) => + event.kind === "runtime.timing.recorded" && + event.native?.eventName === "provider.turn_start", + ) ) { turnTimingEntered.resolve(); await turnTimingGate.promise; @@ -948,9 +952,10 @@ describe("OpenAI app-server startup", () => { ); const started = harness.events.find((event) => event.kind === "run.started"); const publicTurnId = timing?.native?.turnId; - expect(isDriverId(publicTurnId)).toBe(true); + expect(publicTurnId).not.toBe(nativeTurnId); + expect(Buffer.byteLength(publicTurnId ?? "", "utf8")).toBeLessThanOrEqual(256); expect(started?.native?.turnId).toBe(publicTurnId); - expect(timing?.sourceEventId).toBe(`openai.provider.turn_start:${publicTurnId}`); + expect(isDriverId(timing?.sourceEventId)).toBe(true); expect( harness.events.every((event) => Buffer.byteLength(JSON.stringify(event)) < 1_048_576), ).toBe(true); diff --git a/tests/protocol-id.test.ts b/tests/protocol-id.test.ts index 9158670..54f439b 100644 --- a/tests/protocol-id.test.ts +++ b/tests/protocol-id.test.ts @@ -1,6 +1,14 @@ import { describe, expect, test } from "bun:test"; -import { isDriverId, normalizeDriverId, parseDriverId } from "../src/protocol/id"; +import { + createDriverIdFromBytes, + driverIdTimeMs, + isDriverId, + normalizeDriverId, + parseDriverId, +} from "../src/protocol/id"; +import { createRuntimeAssistantMessageId } from "../src/runtimes/runtime-turn-transcript"; +import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; describe("Driver IDs", () => { test("accepts the full ULID range and canonicalizes lowercase input", () => { @@ -18,4 +26,30 @@ describe("Driver IDs", () => { expect(isDriverId(value)).toBe(false); }, ); + + test("encodes deterministic 128-bit identities as canonical Driver IDs", () => { + expect(createDriverIdFromBytes(new Uint8Array(16))).toBe("00000000000000000000000000"); + expect(createDriverIdFromBytes(new Uint8Array(16).fill(0xff))).toBe( + "7ZZZZZZZZZZZZZZZZZZZZZZZZZ", + ); + expect(driverIdTimeMs(parseDriverId("7ZZZZZZZZZ0000000000000000"))).toBe(281_474_976_710_655); + expect(() => createDriverIdFromBytes(new Uint8Array(15))).toThrow("exactly 16 bytes"); + }); + + test("domain-separates deterministic runtime message identities", () => { + const key = 'item:["a","b:c"]'; + const messageId = createRuntimeAssistantMessageId( + DRIVER_TEST_IDS.sessionId, + "openai-message", + key, + ); + + expect(messageId).toBe("10DF94GHQDTF5928TMBEZSWHQ4"); + expect( + createRuntimeAssistantMessageId(DRIVER_TEST_IDS.sessionId, "openai-reasoning", key), + ).not.toBe(messageId); + expect( + createRuntimeAssistantMessageId(DRIVER_TEST_IDS.sessionId, "openai-message", `${key}:next`), + ).not.toBe(messageId); + }); }); From 5e661259e853663e561fed167aa495e792450045 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Mon, 31 Aug 2026 18:25:19 +0000 Subject: [PATCH 18/20] fix(runtime): preserve snapshots across cancellation boundaries --- README.md | 2 +- .../claude/agent-sdk-message-translator.ts | 2 +- src/runtimes/claude/agent-sdk-task-events.ts | 12 ++--- src/surfaces/cma-sdk/sse-bytes-decoder.ts | 3 ++ ...t-sdk-provider-fixtures-transcript.test.ts | 3 +- tests/claude-agent-sdk-task-events.test.ts | 40 ++++++++++------- tests/cma-sdk.test.ts | 44 +++++++++++++++++++ 7 files changed, 81 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 7ef66db..48a62bc 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ Protocol-only races such as ACP load replay barriers, burst updates, and event-d - `vp run test:live:opencode` runs all configured OpenCode compatibility models plus one representative lifecycle model. - `vp run test:live:artifact` tests the artifact path supplied by `AGENT_DRIVER_LIVE_ARTIFACT` without rebuilding it. -The release workflow extracts the packed NPM archive to `packed/` and blocks image and package publication unless `packed/dist/driver.mjs` passes the provider-free MCP artifact test. +The release workflow extracts the packed NPM archive to `packed/`, verifies its declarations, runs the provider-free MCP artifact test, and blocks image and package publication until the same `packed/dist/driver.mjs` passes the complete OpenAI, Claude, and OpenCode live matrix. ## License diff --git a/src/runtimes/claude/agent-sdk-message-translator.ts b/src/runtimes/claude/agent-sdk-message-translator.ts index 146aaf4..452f8f3 100644 --- a/src/runtimes/claude/agent-sdk-message-translator.ts +++ b/src/runtimes/claude/agent-sdk-message-translator.ts @@ -686,7 +686,7 @@ export class ClaudeAgentSdkMessageTranslator { ): Promise { const { diagnostic, snapshot } = projectClaudeBackgroundTasksSnapshot(message); await this.#options.push(context, "driver.claude.tasks.replaced", [ - snapshot, + ...(snapshot === undefined ? [] : [snapshot]), ...(diagnostic === undefined ? [] : [diagnostic]), ]); } diff --git a/src/runtimes/claude/agent-sdk-task-events.ts b/src/runtimes/claude/agent-sdk-task-events.ts index 72465c0..f01981e 100644 --- a/src/runtimes/claude/agent-sdk-task-events.ts +++ b/src/runtimes/claude/agent-sdk-task-events.ts @@ -13,7 +13,7 @@ const MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS = 256; export interface ClaudeBackgroundTasksProjection { readonly diagnostic?: DriverEventInput; - readonly snapshot: DriverEventInput; + readonly snapshot?: DriverEventInput; } function taskSnapshotDiagnostic(code: string, taskCount: number): DriverEventInput { @@ -33,10 +33,9 @@ function taskSnapshotDiagnostic(code: string, taskCount: number): DriverEventInp return event; } -function rejectedTaskSnapshot(code: string, taskCount: number): ClaudeBackgroundTasksProjection { +function rejectedTaskProjection(code: string, taskCount: number): ClaudeBackgroundTasksProjection { return { diagnostic: taskSnapshotDiagnostic(code, taskCount), - snapshot: claudeBackgroundTasksClosedEvent(), }; } @@ -67,7 +66,10 @@ export function projectClaudeBackgroundTasksSnapshot( message: SDKBackgroundTasksChangedMessage, ): ClaudeBackgroundTasksProjection { if (message.tasks.length > MAX_CLAUDE_BACKGROUND_TASK_ENTRIES) { - return rejectedTaskSnapshot("claude.background_tasks_snapshot_too_large", message.tasks.length); + return rejectedTaskProjection( + "claude.background_tasks_snapshot_too_large", + message.tasks.length, + ); } const tasks = new Map< @@ -92,7 +94,7 @@ export function projectClaudeBackgroundTasksSnapshot( ...(title === undefined ? {} : { title }), }); if (tasks.size > MAX_CLAUDE_VISIBLE_BACKGROUND_TASKS) { - return rejectedTaskSnapshot("claude.visible_background_tasks_too_many", tasks.size); + return rejectedTaskProjection("claude.visible_background_tasks_too_many", tasks.size); } } diff --git a/src/surfaces/cma-sdk/sse-bytes-decoder.ts b/src/surfaces/cma-sdk/sse-bytes-decoder.ts index 27676d3..44f977f 100644 --- a/src/surfaces/cma-sdk/sse-bytes-decoder.ts +++ b/src/surfaces/cma-sdk/sse-bytes-decoder.ts @@ -150,6 +150,7 @@ export async function* decodeCmaSseBytes( } for (const record of append(chunk.value)) { + signal?.throwIfAborted(); yield record; } } @@ -162,6 +163,7 @@ export async function* decodeCmaSseBytes( const record = consume(separator); if (record) { + signal?.throwIfAborted(); yield record; } } @@ -170,6 +172,7 @@ export async function* decodeCmaSseBytes( const record = parseSseRecord(decoder.decode(frame.subarray(0, frameLength))); if (record) { + signal?.throwIfAborted(); yield record; } } diff --git a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts index 0150819..f64698f 100644 --- a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts @@ -191,7 +191,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); }); - test("clears the prior task snapshot when the visible-task bound is exceeded", async () => { + test("preserves the prior task snapshot when the visible-task bound is exceeded", async () => { const { context, events, translator } = createHarness(); const message = (tasks: Array<{ description: string; task_id: string; task_type: string }>) => ({ @@ -221,7 +221,6 @@ describe("Claude Agent SDK provider fixtures", () => { expect(events().filter((event) => event.kind === "agent.tasks.replaced")).toMatchObject([ { payload: { tasks: [{ taskId: "task-1" }] } }, - { payload: { tasks: [] } }, ]); expect(events().filter((event) => event.kind === "diagnostic.reported")).toMatchObject([ { payload: { code: "claude.visible_background_tasks_too_many" } }, diff --git a/tests/claude-agent-sdk-task-events.test.ts b/tests/claude-agent-sdk-task-events.test.ts index 2e27569..1ff72f7 100644 --- a/tests/claude-agent-sdk-task-events.test.ts +++ b/tests/claude-agent-sdk-task-events.test.ts @@ -37,7 +37,11 @@ function projection(tasks: SDKBackgroundTasksChangedMessage["tasks"]) { } function snapshot(tasks: SDKBackgroundTasksChangedMessage["tasks"]) { - return projection(tasks).snapshot; + const event = projection(tasks).snapshot; + if (event === undefined) { + throw new Error("Expected a Claude task snapshot."); + } + return event; } describe("Claude Agent SDK task snapshots", () => { @@ -97,37 +101,38 @@ describe("Claude Agent SDK task snapshots", () => { expect(maximum).toMatchObject({ kind: "agent.tasks.replaced" }); expect((maximum.payload as { tasks: unknown[] }).tasks).toHaveLength(256); - expect( - projection(Array.from({ length: 257 }, (_, index) => task(`visible-${index}`))), - ).toMatchObject({ + const tooManyVisible = projection( + Array.from({ length: 257 }, (_, index) => task(`visible-${index}`)), + ); + expect(tooManyVisible).toMatchObject({ diagnostic: { kind: "diagnostic.reported", payload: { code: "claude.visible_background_tasks_too_many" }, visibility: "owner_debug", }, - snapshot: claudeBackgroundTasksClosedEvent(), }); + expect(tooManyVisible.snapshot).toBeUndefined(); const unreadTail = task("unread-tail"); Object.defineProperty(unreadTail, "ambient", { get() { throw new Error("Tasks after the visible limit must not be inspected."); }, }); - expect( - projection([ - ...Array.from({ length: 257 }, (_, index) => task(`bounded-${index}`)), - unreadTail, - ]), - ).toMatchObject({ + const unread = projection([ + ...Array.from({ length: 257 }, (_, index) => task(`bounded-${index}`)), + unreadTail, + ]); + expect(unread).toMatchObject({ diagnostic: { payload: { code: "claude.visible_background_tasks_too_many" } }, - snapshot: claudeBackgroundTasksClosedEvent(), }); - expect( - projection(Array.from({ length: 1_025 }, (_, index) => task(`ambient-${index}`, "x", true))), - ).toMatchObject({ + expect(unread.snapshot).toBeUndefined(); + const tooManyEntries = projection( + Array.from({ length: 1_025 }, (_, index) => task(`ambient-${index}`, "x", true)), + ); + expect(tooManyEntries).toMatchObject({ diagnostic: { payload: { code: "claude.background_tasks_snapshot_too_large" } }, - snapshot: claudeBackgroundTasksClosedEvent(), }); + expect(tooManyEntries.snapshot).toBeUndefined(); expect( projection([ ...Array.from({ length: 1_023 }, (_, index) => task(`ambient-${index}`, "x", true)), @@ -155,6 +160,9 @@ describe("Claude Agent SDK task snapshots", () => { visibility: "participant", }, }); + if (oversizedMetadata.snapshot === undefined) { + throw new Error("Expected a membership-only Claude task snapshot."); + } expect( (oversizedMetadata.snapshot.payload as { tasks: Array> }).tasks, ).toEqual(Array.from({ length: 100 }, (_, index) => ({ taskId: `large-${index}` }))); diff --git a/tests/cma-sdk.test.ts b/tests/cma-sdk.test.ts index 639e30f..90198c0 100644 --- a/tests/cma-sdk.test.ts +++ b/tests/cma-sdk.test.ts @@ -250,6 +250,50 @@ describe("CMA SDK client", () => { expect(resumed).toEqual([second]); }); + test("does not yield buffered server-sent events after cancellation", async () => { + const controller = new AbortController(); + const record = (id: string) => ({ + command: null, + commandResult: null, + commandStatus: null, + createdAt: "2026-01-01T00:00:00.000Z", + cursor: createDriverId(), + direction: "outbound", + event: { + message: { content: id }, + sourceEventKind: "message.completed", + type: "agent.message", + }, + id, + sessionId: "session-1", + updatedAt: "2026-01-01T00:00:00.000Z", + }); + const bytes = new TextEncoder().encode( + [record("event-1"), record("event-2")] + .map((event) => `data: ${JSON.stringify(event)}\n\n`) + .join(""), + ); + const client = new CmaSdkClient({ + baseUrl: "https://driver.test", + fetch: async () => + new Response( + new ReadableStream({ + start(stream) { + stream.enqueue(bytes); + }, + }), + ), + }); + const iterator = client + .streamSessionEvents("session-1", { signal: controller.signal }) + [Symbol.asyncIterator](); + + await expect(iterator.next()).resolves.toMatchObject({ value: { id: "event-1" } }); + const reason = new Error("stop buffered delivery"); + controller.abort(reason); + await expect(iterator.next()).rejects.toBe(reason); + }); + test.each( ["\r\n", "\r", "\n"].flatMap((lineEnding) => ["\r\n", "\r", "\n"] From 0c0c757ce4701003e70d4e886a26e507d7bcabb8 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Tue, 1 Sep 2026 18:41:11 +0000 Subject: [PATCH 19/20] refactor(runtime)!: align latest agent SDK lifecycles --- Containerfile | 4 +- bun.lock | 36 +- package.json | 4 +- scripts/sync-openai-generated.mjs | 23 +- src/bin/driver-process.ts | 20 +- src/core/agent-driver-backend.ts | 12 +- src/core/agent-driver-kernel.ts | 23 +- src/core/driver-command-dispatcher.ts | 41 +- src/core/driver-runtime-io.ts | 36 +- src/core/driver-terminal-state.ts | 14 +- src/core/driver-turn-cancelled-error.ts | 18 +- src/index.ts | 43 +- .../runtime/driver-instance-socket.ts | 48 +- src/protocol/orpc/index.ts | 33 +- .../acp/acp-assistant-transcript-state.ts | 8 +- src/runtimes/acp/acp-driver-backend.ts | 21 +- src/runtimes/acp/acp-turn-controller.ts | 185 +++++-- .../claude/agent-sdk-driver-backend.ts | 213 +++++--- src/runtimes/claude/agent-sdk-event-writer.ts | 27 +- .../claude/agent-sdk-message-translator.ts | 98 ++-- src/runtimes/driver-event-publisher.ts | 83 ++- src/runtimes/mcp/remote-http-mcp-executor.ts | 14 +- src/runtimes/openai/app-server-client.ts | 72 ++- .../openai/app-server-driver-backend.ts | 319 +++++++----- .../openai/app-server-event-bridge.ts | 141 +++++- .../openai/app-server-protocol-types.ts | 4 - .../openai/app-server-turn-tracker.ts | 242 ++++++++- .../openai/generated-json-schema/README.md | 2 +- .../ServerNotification.json | 48 ++ .../generated-json-schema/ServerRequest.json | 15 + .../openai/generated/ProtocolMethods.ts | 2 + src/runtimes/openai/generated/README.md | 2 +- tests/acp-driver-backend.test.ts | 474 +++++++++++++++++- tests/acp-provider-fixtures.test.ts | 18 +- tests/agent-driver-kernel-commands.test.ts | 15 +- tests/architecture-boundaries.test.ts | 186 +++---- tests/claude-agent-sdk-driver-backend.test.ts | 467 +++++++++++++++-- tests/claude-agent-sdk-durability.test.ts | 4 +- ...ent-sdk-provider-fixtures-terminal.test.ts | 72 +-- ...t-sdk-provider-fixtures-transcript.test.ts | 102 ++-- tests/claude-agent-sdk-test-helpers.ts | 43 +- tests/driver-artifact-contract.test.ts | 101 +--- tests/driver-artifact-live.test.ts | 39 +- tests/driver-artifact-test-controller.test.ts | 94 +--- tests/driver-artifact-test-controller.ts | 52 +- .../driver-event-publisher-admission.test.ts | 163 +++++- tests/driver-event-test-helpers.ts | 44 ++ tests/driver-rpc-wire-v3.test.ts | 206 +++----- ...driver-runtime-boundary-dispatcher.test.ts | 17 +- tests/driver-runtime-boundary-fixtures.ts | 4 +- tests/driver-runtime-boundary-state.test.ts | 103 ++-- tests/driver-runtime-boundary-timing.test.ts | 4 +- tests/driver-terminal-state.test.ts | 5 +- ...penai-app-server-agent-task-events.test.ts | 95 +--- tests/openai-app-server-client.test.ts | 4 +- .../openai-app-server-event-bridge-fixture.ts | 25 +- ...enai-app-server-event-bridge-items.test.ts | 52 ++ ...app-server-event-bridge-transcript.test.ts | 53 ++ ...penai-app-server-provider-fixtures.test.ts | 6 +- .../openai-app-server-request-handler.test.ts | 25 + tests/openai-app-server-startup.test.ts | 238 +++++++-- tests/public-api-consumer.ts | 36 +- tests/public-api.test.ts | 149 +++--- 63 files changed, 3155 insertions(+), 1592 deletions(-) create mode 100644 tests/driver-event-test-helpers.ts diff --git a/Containerfile b/Containerfile index fe3e3fc..6ee9c02 100644 --- a/Containerfile +++ b/Containerfile @@ -4,9 +4,9 @@ FROM docker.io/oven/bun:${BUN_VERSION}@sha256:5ff609364c049b54eb0ff560ec96319729 FROM docker.io/cloudflare/sandbox:0.12.9@sha256:4a56a37a3cfd9b38d65bb4b5d0b341e6490a3a4c0226274ae4c1cca4948e85fe # Keep this pin in sync with downstream mosoo apps/api/package.json -> @cloudflare/sandbox. -ARG CLAUDE_AGENT_SDK_VERSION=0.3.251 +ARG CLAUDE_AGENT_SDK_VERSION=0.3.252 ARG BUN_VERSION -ARG OPENAI_RUNTIME_VERSION=0.151.0 +ARG OPENAI_RUNTIME_VERSION=0.152.0 ARG OPENCODE_VERSION=1.18.25 COPY --from=bun-runtime /usr/local/bin/bun /usr/local/bin/bun diff --git a/bun.lock b/bun.lock index 63be142..1b0b043 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@mosoo/agent-driver", "dependencies": { "@agentclientprotocol/sdk": "1.4.0", - "@anthropic-ai/claude-agent-sdk": "0.3.251", + "@anthropic-ai/claude-agent-sdk": "0.3.252", "@anthropic-ai/sdk": "0.122.0", "@modelcontextprotocol/client": "^2.0.0", "@orpc/client": "^1.15.0", @@ -15,7 +15,7 @@ "zod": "4.5.4", }, "devDependencies": { - "@openai/codex": "0.151.0", + "@openai/codex": "0.152.0", "@types/bun": "1.4.0", "@types/node": "^26.3.0", "opencode-ai": "1.18.25", @@ -27,23 +27,23 @@ "packages": { "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.4.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-/eufudw+aFY1LKLolT6yFE6UMmYRl7fMJ/DEONSIyR6wI3slHWITBsANRGqXEY8FRzqUxwh7QEaGiZHcJPVThg=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.251", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.251", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.251", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.251", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.251", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.251" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-DqSi8mH2tQYRlVV0G+lJnQ/WbjJZ/a+8cJ3vPuYoqh8esIIvXHm1ZOXV1UPGsFYRnbBytEoiSGitguEXd+sQ+Q=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.252", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.252", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.252", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.252", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.252", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.252", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.252", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.252", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.252" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-hCkyZFn3J46aAMNqS6AZbYz91FaLUmX5VvJOzYZqzlVBJN47OxXQugqOzqa6b6GOZRmwiqW2ck8J8TE7bQZswQ=="], - "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.251", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C23h+Dbddcc4gai95+lhm4/94am2IOq+bf3IdEDDS7EPqDQqajo2s5q1/ZSwq9rOc0RJLT7Ws72ZCzYJh67KSg=="], + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.252", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7lcngnoB3o3ex3lCt54mVhmL4u7ieBqd2g0sLF4YU8GdZap6skJMphe+7p8+upnpmTNJb7QWJYa6+FqINdMknQ=="], - "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.251", "", { "os": "darwin", "cpu": "x64" }, "sha512-HrLnb3ggk+vMbymUvbosgPmxWp4W6Ot+0mNVjoBYDn5OZrTV3C3LRtbWf7jwcGyKMcg0xJf0AqiudQ2BRrlr8A=="], + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.252", "", { "os": "darwin", "cpu": "x64" }, "sha512-jznf/im+Ry47vHao6+U8RT2w5gEjxs1ut3EjaoSRVyn35iK40VcjCPjLG7WQMNSqkuxFLTLGxNlKesOw4gXQQg=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.251", "", { "os": "linux", "cpu": "arm64" }, "sha512-3E27F5j82EWOyEniRW7crmo0jWYYQmPDdP52jWq6Y6aytiyYIdUFMEGZZ6chVHNlZiU7GsjTa3teKk2xlQ68lQ=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.252", "", { "os": "linux", "cpu": "arm64" }, "sha512-yXet0aNf32MaGJcJvQqMfLv4mfrnBqdKPa1f/KSPQQJA/7v3EFKd2OlBguSwmBJAd0uB671G0PBThzlzcIEiTA=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.251", "", { "os": "linux", "cpu": "arm64" }, "sha512-RTUf7TPBUkQ6oV3pDkEdcD47I0uH0ZpvmZlXHnrLGznFqyLr+7XHupOxUMJD0Jwm9v5qeqpNiPAYB0dL4RYiHg=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.252", "", { "os": "linux", "cpu": "arm64" }, "sha512-BnFd6MTl2Egid7JuISaJKSmgz8jVm3J/qrnN6u+CKia42k7h5gKawKLi+Jh6FanZQKRTfq/mAQMjiARylaKeMA=="], - "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.251", "", { "os": "linux", "cpu": "x64" }, "sha512-qCD87XPjcNM1u4ukqmcgpHDl3Y6l+cW8j7kPzH91Y5yLBYJ5xYZA2KtJ7AYNEPOteVyOGJw/efmva0S8CRr0fg=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.252", "", { "os": "linux", "cpu": "x64" }, "sha512-pSwulGf0yQJu02FzlV/mVoGH2HQMHsBmIhmnKtYF5/zp22vF1vnFANCtUZjyNnvAhmCnkNJRhOe76pzlaRd89w=="], - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.251", "", { "os": "linux", "cpu": "x64" }, "sha512-SzXrdy7jjrjJIuTG+VMRAY0YZ3G/8w21WuhAozwnvnEUAPo6NDv6lgFinu7NO8J6CPE+MK2sGze6JeCF+ljgUg=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.252", "", { "os": "linux", "cpu": "x64" }, "sha512-SsFTpHDjNCtpCVAlcFp/gwZj8WVsbtORuPZoKujlxNOHjE1uJ9FLXhyo8grFsfxMnyp1QCdsFmFIVt1YO4NDxw=="], - "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.251", "", { "os": "win32", "cpu": "arm64" }, "sha512-FH1ZLcyc97ie3h7CSlIxA1ppXILgf0V8sFQrWnHyKBrthJXQkMHXhsTq9OZV/2KhXslNf2hTe0gHpZ1nCL1Gmg=="], + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.252", "", { "os": "win32", "cpu": "arm64" }, "sha512-oxtZaKcHiRKI5OIfAogIgw8oNCC6U1E7jqUsl1FvXG72JSMrRFnMViXuE5EVZoJH8m6NkFqzxVuVB7gf8HZZgw=="], - "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.251", "", { "os": "win32", "cpu": "x64" }, "sha512-/jmtFIvfF0UFMxSZ8WV2Aus8aGA3+8Ft6AHvWuBJkY5jM2ubfqi6GnBjKCAL831TTllp2rKZlViANtWWRBvpvg=="], + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.252", "", { "os": "win32", "cpu": "x64" }, "sha512-2Lv3mzxXP1OwY9ZXvKy8Fr1GE8UQS8mKp8lvDexnFfSqVkfs3BfB2qqfuv81H9QCIDX3vu/G+dJKKQxgElrTzA=="], "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.122.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-GGPNftt0caaz9MDlmNQGHX8855Ojaduyy5pm9Sm1h7HalCn0cWNb5/bweadJF+4yzbal+QL6ztBa09WAAOzLmQ=="], @@ -65,19 +65,19 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], - "@openai/codex": ["@openai/codex@0.151.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.151.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.151.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.151.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.151.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.151.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.151.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-mhtWmOZRdmWD1jPbLDnQb59BsaVP/V+lXe/OFNR9ZcLZU0UCiBwn98Fcav1ss7sDIlHkuqj6nWd44IPeXoOhJA=="], + "@openai/codex": ["@openai/codex@0.152.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.152.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.152.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.152.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.152.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.152.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.152.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-Vx0tg/J5SbxYYGJazTtL/XySK9Dlqc5KW1MZM71NMwVci/4F1ap+FfSKPFTlrICEtOTuq3KNcWSdv9oMGdPuRw=="], - "@openai/codex-darwin-arm64": ["@openai/codex@0.151.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-g7YzpaCZGCw19R/gly3vRPjnLqaW7JcBAu2WQQ6e8PIlvBPmS/gMplIUURMgNO6gi8LsPzdlQtLqkwoeOOlIdg=="], + "@openai/codex-darwin-arm64": ["@openai/codex@0.152.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DOnDA6EKOs+aRytYH4ffIuUutok9ovpvCNax8aaTZvBB2bKUPjUleLrP1pk+hGarddqU1ySuvXMOfp6ufXyOVQ=="], - "@openai/codex-darwin-x64": ["@openai/codex@0.151.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-0y+g8TVpP+Fn10mjoKYXER6qYjn29w7xBUsbPXJ6Accu/FoM4Qp4WbKXQPmE0G0yUACTQVZRjzTSsdWUezNgkg=="], + "@openai/codex-darwin-x64": ["@openai/codex@0.152.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-p2XLFWU+Lflke89zNK67ad2/yaFndUIZkyk+gjl35NCDz+LncvAUOeiTFB2MtmC2kxvudcYo6/yezQqwM29Gnw=="], - "@openai/codex-linux-arm64": ["@openai/codex@0.151.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-CsLgFeX4TQ6I2Gdrxd2r5UbgIbDLCdtcLAlnMYjr06bCL057MTNGec7Ewb3+Z2DBiMuXCljdTBGqLOePkMV0sQ=="], + "@openai/codex-linux-arm64": ["@openai/codex@0.152.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-OTmO6y5gCpcjzybwzgK/nSM8VI68Pw+T2ohBK+8cwVM+7PCRqSqtNo7qTLG0bgv9ff6QtwiZ9sl0KtY42H0Wqg=="], - "@openai/codex-linux-x64": ["@openai/codex@0.151.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-xcVyY1FtwvVYhh2JBmz8fX8CQqFAxO/lxJ2IXsh8x5uwxZVHVl5fZHFHf8JdRaOGG0vpkYmu/DKKVoLd56/DDQ=="], + "@openai/codex-linux-x64": ["@openai/codex@0.152.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-Isn/g5EZTaNbwZtaIuz67U1FNDEVVeMpS4xiR+c1dpOPL2xxrGNtdAothOW2YKsb96OsS+QfClziFb4qxesWDA=="], - "@openai/codex-win32-arm64": ["@openai/codex@0.151.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-zDWzOoh9wHm+Om1Nhn7os47rAVeSGPh0SnM3YOttdq6iPJz2zn4vBnbGUZjeih1qW/3mvNF3Oyd4owlaHmphmg=="], + "@openai/codex-win32-arm64": ["@openai/codex@0.152.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-1aMFMFvhSru5IT3hRstBjTmLPkhi+FKlu05bKKsKY+Pmx3a0sZBiK8WYFkROUynsM6ZVgNApfDv8zVUbX5crLA=="], - "@openai/codex-win32-x64": ["@openai/codex@0.151.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-sLT7xvID3jhU6tkzcwRPnMEclKRwUPbpo0mtfxIF9KpdZH3VJV7sM2/kXWXyvUM7Zt/YeyOaeATTEysbRz8Yog=="], + "@openai/codex-win32-x64": ["@openai/codex@0.152.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-vy67ua+QeOHKqT5ovLOkINncuaknldUkOfwcGdxQwPbSxgnCoiHnAO/rieWQt3ncM2pRzygtPVoLOFiovcRlaQ=="], "@orpc/client": ["@orpc/client@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0", "@orpc/standard-server-fetch": "1.15.0", "@orpc/standard-server-peer": "1.15.0" } }, "sha512-Qt0FdPSGySdQwy83iUWOq+Iqhw2gM9qHtyxfBbcw1mwOz1s4O2BwUFA3ymVTLIRRNYRgPrQLaBZhp8CacqeePg=="], diff --git a/package.json b/package.json index 0692fc0..61fad37 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "1.4.0", - "@anthropic-ai/claude-agent-sdk": "0.3.251", + "@anthropic-ai/claude-agent-sdk": "0.3.252", "@anthropic-ai/sdk": "0.122.0", "@modelcontextprotocol/client": "^2.0.0", "@orpc/client": "^1.15.0", @@ -102,7 +102,7 @@ "zod": "4.5.4" }, "devDependencies": { - "@openai/codex": "0.151.0", + "@openai/codex": "0.152.0", "@types/bun": "1.4.0", "@types/node": "^26.3.0", "opencode-ai": "1.18.25", diff --git a/scripts/sync-openai-generated.mjs b/scripts/sync-openai-generated.mjs index 830c3ed..c2f8f64 100644 --- a/scripts/sync-openai-generated.mjs +++ b/scripts/sync-openai-generated.mjs @@ -1,4 +1,4 @@ -import { spawnSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; import { copyFileSync, cpSync, @@ -30,26 +30,13 @@ const schemaSources = new Map([ ]); function run(command, args) { - const result = spawnSync(command, args, { cwd: root, encoding: "utf8" }); - if (result.status !== 0) { - throw new Error( - [result.stdout, result.stderr, `${command} exited with ${String(result.status)}.`] - .filter(Boolean) - .join("\n"), - ); - } + execFileSync(command, args, { cwd: root, stdio: "inherit" }); } function listFiles(directory, extension) { - return readdirSync(directory, { withFileTypes: true }) - .flatMap((entry) => { - const path = resolve(directory, entry.name); - return entry.isDirectory() - ? listFiles(path, extension) - : path.endsWith(extension) - ? [path] - : []; - }) + return readdirSync(directory, { recursive: true, withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith(extension)) + .map((entry) => resolve(entry.parentPath, entry.name)) .sort(); } diff --git a/src/bin/driver-process.ts b/src/bin/driver-process.ts index 8a6a7fc..c66671a 100644 --- a/src/bin/driver-process.ts +++ b/src/bin/driver-process.ts @@ -332,23 +332,15 @@ export class DriverProcess { this.#heartbeatLoop.stop(this.#logger, reason); try { - let permissionFailure: { error: unknown } | null = null; - - try { - await permissionCancellation; - } catch (error) { - permissionFailure = { error }; - } - - const backendShutdown = await Promise.allSettled([this.#backendLifecycle?.shutdown(reason)]); + const [permissionResult] = await Promise.allSettled([permissionCancellation]); + const [backendResult] = await Promise.allSettled([this.#backendLifecycle?.shutdown(reason)]); socket.abortPendingRequests(reason); - const failure = backendShutdown.find((result) => result.status === "rejected"); - if (permissionFailure !== null) { - throw permissionFailure.error; + if (permissionResult.status === "rejected") { + throw permissionResult.reason; } - if (failure?.status === "rejected") { - throw failure.reason; + if (backendResult.status === "rejected") { + throw backendResult.reason; } if (this.#runtimeState.status() === "stopping") { diff --git a/src/core/agent-driver-backend.ts b/src/core/agent-driver-backend.ts index c20270d..cdeec52 100644 --- a/src/core/agent-driver-backend.ts +++ b/src/core/agent-driver-backend.ts @@ -1,11 +1,8 @@ import type { AgentDriverCommandSource, AgentDriverEventSink, - AgentDriverFilePort, AgentDriverHostPorts, - AgentDriverMcpPort, AgentDriverPermissionPort, - AgentDriverSkillPort, } from "../host-ports"; import type { Logger } from "../observability"; import type { DriverEventInput } from "../protocol/events"; @@ -27,14 +24,7 @@ export interface AgentDriverLifecycle { fail(error: Error): void; } -export type AgentDriverContextPortOverrides = Partial<{ - commandSource: AgentDriverCommandSource; - eventSink: AgentDriverEventSink; - file: AgentDriverFilePort; - mcp: AgentDriverMcpPort; - permission: AgentDriverPermissionPort; - skill: AgentDriverSkillPort; -}>; +export type AgentDriverContextPortOverrides = Partial; export interface AgentDriverContextInput { commandSource?: AgentDriverCommandSource; diff --git a/src/core/agent-driver-kernel.ts b/src/core/agent-driver-kernel.ts index c2b2c18..2d6bbe4 100644 --- a/src/core/agent-driver-kernel.ts +++ b/src/core/agent-driver-kernel.ts @@ -141,8 +141,9 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo claimRunCancellation( ticket: DriverRunTicket, reason: string, + source?: Parameters[2], ): "already_claimed" | "claimed" | "terminal_selected" { - return this.#terminalState.claimCancellation(ticket, reason); + return this.#terminalState.claimCancellation(ticket, reason, source); } async cancel(reason: string): Promise { @@ -905,22 +906,14 @@ export class AgentDriverKernelCore implements AgentDriverKernel, DriverRuntimeIo this.#commands.close({ discard: true }); try { - let permissionFailure: { error: unknown } | null = null; + const [permissionResult] = await Promise.allSettled([permissionCancellation]); + const [backendResult] = await Promise.allSettled([this.#backendLifecycle?.shutdown(reason)]); - try { - await permissionCancellation; - } catch (error) { - permissionFailure = { error }; - } - - const backendShutdown = await Promise.allSettled([this.#backendLifecycle?.shutdown(reason)]); - const failure = backendShutdown.find((result) => result.status === "rejected"); - - if (permissionFailure !== null) { - throw permissionFailure.error; + if (permissionResult.status === "rejected") { + throw permissionResult.reason; } - if (failure?.status === "rejected") { - throw failure.reason; + if (backendResult.status === "rejected") { + throw backendResult.reason; } await this.#completeShutdown(); diff --git a/src/core/driver-command-dispatcher.ts b/src/core/driver-command-dispatcher.ts index b1fe40a..ad2625d 100644 --- a/src/core/driver-command-dispatcher.ts +++ b/src/core/driver-command-dispatcher.ts @@ -43,6 +43,7 @@ import { import type { DriverRuntimeStateMachine } from "./driver-runtime-state"; import { isDriverTurnCancelledError } from "./driver-runtime-state"; import type { DriverInputOutcome, DriverRunTicket } from "./driver-terminal-state"; +import type { DriverTurnCancellationSource } from "./driver-turn-cancelled-error"; interface DriverCommandDispatcherOptions { backend: AgentDriverBackend; @@ -59,7 +60,9 @@ interface DriverCommandDispatcherOptions { const COMMAND_POLL_INTERVAL_MS = 250; export const ACTIVE_TURN_CANCEL_GRACE_MS = 2_000; -const ACTIVE_INPUT_SETTLE_GRACE_MS = ACTIVE_TURN_CANCEL_GRACE_MS + DRIVER_EVENT_DELIVERY_TIMEOUT_MS; +// Fault-containment ceiling, not a normal cancellation SLA: an admitted turn can +// legitimately cross several independent durable-delivery and cleanup epochs. +export const ACTIVE_INPUT_SETTLE_GRACE_MS = DRIVER_EVENT_DELIVERY_TIMEOUT_MS * 9; const EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS = 2_000; export const ACTIVE_MCP_COMMIT_GRACE_MS = AGENT_DRIVER_MCP_EXECUTE_TIMEOUT_MS + 30_000; const MAX_ACTIVE_MCP_COMMANDS = 32; @@ -158,6 +161,7 @@ export class DriverCommandDispatcher { readonly #shutdownSignal: AbortSignal; readonly #shutdown: (socket: DriverRuntimeIo, reason: string) => Promise; readonly #commandDelivery: DriverCommandDelivery; + #activeWorkFenceFailure: { error: unknown } | null = null; #activeWorkFailure: { error: unknown } | null = null; #activeRunTicket: DriverRunTicket | null = null; readonly #activeMcpAdmissions = new Set(); @@ -165,6 +169,7 @@ export class DriverCommandDispatcher { readonly #mcpRunFailures = new Map(); readonly #terminalRunFences = new Set(); #activeRunGeneration = 0; + #activeRunSettleTask: Promise | null = null; #activeRunTask: Promise | null = null; #shutdownCompleted = false; #shutdownPermissionTask: Promise | null = null; @@ -550,9 +555,6 @@ export class DriverCommandDispatcher { if (command.kind === "mcp.execute" && replay === null && !mcpAdmissionGranted) { throw new Error(`Run ${command.runId} is already publishing its terminal event.`); } - if (replay === null && command.kind === "input.start" && this.#activeRunTask !== null) { - await settleInput(this.#activeRunTask); - } this.#assertCommandRunOwnership(socket, command, replayMode); } catch (error) { await this.#commandDelivery.reject(runtimeContext, command, { @@ -632,6 +634,7 @@ export class DriverCommandDispatcher { .finally(() => { if (this.#activeRunTask === activeRunTask) { this.#activeRunTask = null; + this.#activeRunSettleTask = null; this.#activeRunTicket = null; } }); @@ -740,13 +743,13 @@ export class DriverCommandDispatcher { #abortActiveWork( socket: DriverRuntimeIo, reason: string, - source: "session.stop" | "shutdown" | "turn.cancel", + source: DriverTurnCancellationSource, ): "already_claimed" | "claimed" | "idle" | "terminal_selected" { const ticket = this.#activeRunTicket; const cancellation = ticket === null || socket.runSnapshot(ticket.runId) === null ? "idle" - : socket.claimRunCancellation(ticket, reason); + : socket.claimRunCancellation(ticket, reason, source); if (source !== "turn.cancel" || cancellation !== "terminal_selected") { if (source === "shutdown") { @@ -767,7 +770,7 @@ export class DriverCommandDispatcher { runtimeContext: AgentDriverContext, socket: DriverRuntimeIo, reason: string, - source: "session.stop" | "shutdown" | "turn.cancel", + source: DriverTurnCancellationSource, permissionCancellation?: Promise, ): Promise { const cancellation = this.#abortActiveWork(socket, reason, source); @@ -814,10 +817,14 @@ export class DriverCommandDispatcher { permissionFailure = { error }; } } + if (this.#activeWorkFenceFailure !== null) { + throw this.#activeWorkFenceFailure.error; + } const tasks: Promise[] = []; - if (this.#activeRunTask !== null) { - tasks.push(settleInput(this.#activeRunTask)); + const activeRunSettleTask = this.#settleActiveRun(); + if (activeRunSettleTask !== null) { + tasks.push(activeRunSettleTask); } if (this.#activeMcpCommands.size > 0) { @@ -839,10 +846,24 @@ export class DriverCommandDispatcher { throw permissionFailure.error; } if (failure?.status === "rejected") { - throw failure.reason; + this.#activeWorkFenceFailure ??= { error: failure.reason }; + throw this.#activeWorkFenceFailure.error; } } + #settleActiveRun(): Promise | null { + if (this.#activeRunTask === null) { + return null; + } + + return (this.#activeRunSettleTask ??= settleInput(this.#activeRunTask).catch( + (error: unknown) => { + this.#activeWorkFenceFailure ??= { error }; + throw this.#activeWorkFenceFailure.error; + }, + )); + } + #startMcpCommand( runtimeContext: AgentDriverContext, socket: DriverRuntimeIo, diff --git a/src/core/driver-runtime-io.ts b/src/core/driver-runtime-io.ts index 2175213..0cadae8 100644 --- a/src/core/driver-runtime-io.ts +++ b/src/core/driver-runtime-io.ts @@ -1,3 +1,4 @@ +import type { AgentDriverEventSink } from "../host-ports"; import type { DriverEventInput } from "../protocol/events"; import { createDriverId } from "../protocol/id"; import type { RunId } from "../protocol/id"; @@ -8,19 +9,14 @@ import type { DriverHeartbeatInput, DriverHeartbeatOutput, } from "../protocol/orpc"; -import type { - DriverCommandUpdate, - McpExternalToolEffectClaim, - McpExternalToolEffectSettlement, - McpExternalToolEffectState, - RuntimeCommand, -} from "../runtime-command"; +import type { DriverCommandUpdate, RuntimeCommand } from "../runtime-command"; import type { DriverInputOutcome, DriverInputSettlement, DriverRunSnapshot, DriverRunTicket, } from "./driver-terminal-state"; +import type { DriverTurnCancellationSource } from "./driver-turn-cancelled-error"; export interface DriverRuntimeEventPort { currentRunId(): RunId | null; @@ -170,31 +166,19 @@ export interface DriverRuntimeCommandPort { } /** API-owned durable effect ledger used only for external MCP calls. */ -export interface DriverRuntimeExternalToolEffectPort { - claimExternalToolEffect( - input: { claimToken: string; commandId: string }, - signal: AbortSignal, - ): Promise; - observeExternalToolEffect( - input: { commandId: string }, - signal: AbortSignal, - ): Promise; - settleExternalToolEffect( - input: { - claimToken: string; - commandId: string; - effectId: string; - settlement: McpExternalToolEffectSettlement; - }, - signal: AbortSignal, - ): Promise; -} +export type DriverRuntimeExternalToolEffectPort = Required< + Pick< + AgentDriverEventSink, + "claimExternalToolEffect" | "observeExternalToolEffect" | "settleExternalToolEffect" + > +>; export interface DriverRuntimeRunPort { beginRun(runId: RunId): DriverRunTicket; claimRunCancellation( ticket: DriverRunTicket, reason: string, + source?: DriverTurnCancellationSource, ): "already_claimed" | "claimed" | "terminal_selected"; completeRun(signal?: AbortSignal): Promise; failRun(error: DriverFailureInput["error"], signal?: AbortSignal): Promise; diff --git a/src/core/driver-terminal-state.ts b/src/core/driver-terminal-state.ts index b8a9c7d..8593402 100644 --- a/src/core/driver-terminal-state.ts +++ b/src/core/driver-terminal-state.ts @@ -4,7 +4,10 @@ import type { DriverEventInput } from "../protocol/events"; import type { RunId } from "../protocol/id"; import type { DriverEventReceipt } from "../protocol/orpc"; import type { RunError } from "../runtime-command"; -import { DriverTurnCancelledError } from "./driver-turn-cancelled-error"; +import { + DriverTurnCancelledError, + type DriverTurnCancellationSource, +} from "./driver-turn-cancelled-error"; export type DriverRunTerminalStatus = "cancelled" | "completed" | "failed"; @@ -115,17 +118,24 @@ export class DriverTerminalStateMachine { claimCancellation( ticket: DriverRunTicket, reason: string, + source: DriverTurnCancellationSource = "turn.cancel", ): "already_claimed" | "claimed" | "terminal_selected" { const active = this.#requireRun(ticket); if (active.terminal !== null) { return "terminal_selected"; } if (active.cancellationClaim !== null) { + if ( + source !== "turn.cancel" && + active.cancellation.signal.reason instanceof DriverTurnCancelledError + ) { + active.cancellation.signal.reason.preventResume(); + } return "already_claimed"; } active.cancellationClaim = { reason }; - active.cancellation.abort(new DriverTurnCancelledError(reason)); + active.cancellation.abort(new DriverTurnCancelledError(reason, source)); return "claimed"; } diff --git a/src/core/driver-turn-cancelled-error.ts b/src/core/driver-turn-cancelled-error.ts index 63165f2..5cf8ad9 100644 --- a/src/core/driver-turn-cancelled-error.ts +++ b/src/core/driver-turn-cancelled-error.ts @@ -1,7 +1,23 @@ +export type DriverTurnCancellationSource = "session.stop" | "shutdown" | "turn.cancel"; + export class DriverTurnCancelledError extends Error { - constructor(reason: string) { + readonly #resumeCancellation = new AbortController(); + readonly resumeSignal = this.#resumeCancellation.signal; + + get resumeAllowed(): boolean { + return !this.resumeSignal.aborted; + } + + constructor(reason: string, source: DriverTurnCancellationSource = "turn.cancel") { super(reason); this.name = "DriverTurnCancelledError"; + if (source !== "turn.cancel") { + this.#resumeCancellation.abort(); + } + } + + preventResume(): void { + this.#resumeCancellation.abort(); } } diff --git a/src/index.ts b/src/index.ts index 212e484..d80dcc5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,29 +27,8 @@ export type { CmaUserMessageEvent, CmaUserToolConfirmationEvent, } from "./projections/cma"; -export { - CMA_DEFAULT_BETA_HEADER_NAME, - CMA_DEFAULT_BETA_HEADER_VALUE, - createCmaHttpHandler, -} from "./surfaces/cma-http"; -export type { - CmaHttpAuthorizationContext, - CmaHttpAuthorizer, - CmaHttpBetaHeaderRequirement, - CmaHttpDriverCommandDispatcher, - CmaHttpDriverCommandDispatchInput, - CmaHttpHandler, - CmaHttpHandlerOptions, -} from "./surfaces/cma-http"; -export { CmaSdkClient, CmaSdkError } from "./surfaces/cma-sdk"; -export type { - CmaSdkBetaHeader, - CmaSdkClientOptions, - CmaSdkFetch, - CmaSdkRequestOptions, - CmaSdkStreamOptions, - CmaSessionEventDispatchRecord, -} from "./surfaces/cma-sdk"; +export * from "./surfaces/cma-http"; +export * from "./surfaces/cma-sdk"; export { createCmaMemoryStore } from "./stores/memory"; export type { CmaMemoryStoreIdFactory, CmaMemoryStoreOptions } from "./stores/memory"; export { @@ -98,23 +77,7 @@ export type { AgentDriverContextPortOverrides, } from "./core/agent-driver-backend"; export { createAgentDriverContext } from "./core/agent-driver-backend"; -export { - isSupportedDriverRuntime, - isSupportedDriverRuntimeTransport, - SUPPORTED_DRIVER_NATIVE_RUNTIME_REF_KINDS, - SUPPORTED_DRIVER_RUNTIMES, - SUPPORTED_DRIVER_RUNTIME_TRANSPORTS, -} from "./protocol/runtime"; -export type { - DriverNativeRuntimeRef, - DriverNativeRuntimeRefKind, - DriverRuntime, - DriverRuntimeTransport, -} from "./protocol/runtime"; -export { - getExpectedDriverNativeRuntimeRefKind, - parseDriverNativeRuntimeRef, -} from "./protocol/runtime"; +export * from "./protocol/runtime"; export { parseDriverEventEnvelope } from "./protocol/events"; export type { DriverEvent, DriverEventEnvelope, DriverEventInput } from "./protocol/events"; export type { diff --git a/src/infrastructure/runtime/driver-instance-socket.ts b/src/infrastructure/runtime/driver-instance-socket.ts index a287d4c..5330e64 100644 --- a/src/infrastructure/runtime/driver-instance-socket.ts +++ b/src/infrastructure/runtime/driver-instance-socket.ts @@ -74,19 +74,11 @@ const DRIVER_RPC_TIMEOUT_MS = 10_000; const MAX_WEBSOCKET_CLOSE_REASON_BYTES = 123; function toWebSocketCloseReason(reason: string): string { - let bytes = 0; - let result = ""; - - for (const character of reason) { - const characterBytes = Buffer.byteLength(character, "utf8"); - if (bytes + characterBytes > MAX_WEBSOCKET_CLOSE_REASON_BYTES) { - break; - } - bytes += characterBytes; - result += character; - } - - return result; + const { read } = new TextEncoder().encodeInto( + reason, + new Uint8Array(MAX_WEBSOCKET_CLOSE_REASON_BYTES), + ); + return reason.slice(0, read); } export class DriverInstanceSocket { @@ -184,8 +176,9 @@ export class DriverInstanceSocket { claimRunCancellation( ticket: DriverRunTicket, reason: string, + source?: Parameters[2], ): "already_claimed" | "claimed" | "terminal_selected" { - return this.#terminalState.claimCancellation(ticket, reason); + return this.#terminalState.claimCancellation(ticket, reason, source); } releaseRun(ticket: DriverRunTicket, reason: "command_acked" | "driver_failing"): void { @@ -735,30 +728,9 @@ export class DriverInstanceSocket { #enqueueDelivery(operation: () => Promise, signal?: AbortSignal): Promise { const predecessor = this.#deliveryTail; - const slot = Promise.withResolvers(); - this.#deliveryTail = predecessor.then( - () => slot.promise, - () => slot.promise, - ); - - return (async () => { - let acquired = false; - - try { - await raceWithAbort(predecessor, signal); - acquired = true; - return await operation(); - } finally { - if (acquired) { - slot.resolve(); - } else { - void predecessor.then( - () => slot.resolve(), - () => slot.resolve(), - ); - } - } - })(); + const task = raceWithAbort(predecessor, signal).then(operation); + this.#deliveryTail = Promise.allSettled([predecessor, task]).then(() => {}); + return task; } #requireClient(): DriverRuntimeClient { diff --git a/src/protocol/orpc/index.ts b/src/protocol/orpc/index.ts index d235d22..22931c6 100644 --- a/src/protocol/orpc/index.ts +++ b/src/protocol/orpc/index.ts @@ -240,29 +240,26 @@ const driverFailureInputSchema = z.strictObject({ runId: nonEmptyStringSchema, }); -const driverCommandUpdateIdentitySchema = { +const driverCommandIdentitySchema = { commandId: nonEmptyStringSchema, driverInstanceId: nonEmptyStringSchema, } as const; const driverCommandUpdateInputSchema = z.discriminatedUnion("status", [ - z.strictObject({ ...driverCommandUpdateIdentitySchema, status: z.literal("accepted") }), - z.strictObject({ ...driverCommandUpdateIdentitySchema, status: z.literal("cancelled") }), + z.strictObject({ ...driverCommandIdentitySchema, status: z.literal("accepted") }), + z.strictObject({ ...driverCommandIdentitySchema, status: z.literal("cancelled") }), z.strictObject({ - ...driverCommandUpdateIdentitySchema, + ...driverCommandIdentitySchema, result: runtimeCommandResultSchema.optional(), status: z.literal("completed"), }), z.strictObject({ - ...driverCommandUpdateIdentitySchema, + ...driverCommandIdentitySchema, error: durableRunErrorSchema, status: z.literal("failed"), }), ]); -const driverExternalToolEffectObserveInputSchema = z.strictObject({ - commandId: nonEmptyStringSchema, - driverInstanceId: nonEmptyStringSchema, -}); +const driverExternalToolEffectObserveInputSchema = z.strictObject(driverCommandIdentitySchema); const driverExternalToolEffectIntentSchema = z.strictObject({ effectId: nonEmptyStringSchema, @@ -295,9 +292,8 @@ const driverExternalToolEffectStateSchema = z.discriminatedUnion("kind", [ ]); const driverExternalToolEffectClaimInputSchema = z.strictObject({ + ...driverCommandIdentitySchema, claimToken: lowercaseUuidV4Schema, - commandId: nonEmptyStringSchema, - driverInstanceId: nonEmptyStringSchema, }); const driverExternalToolEffectClaimOutputSchema = z.discriminatedUnion("kind", [ @@ -324,9 +320,8 @@ const driverExternalToolEffectSettlementSchema = z ); const driverExternalToolEffectSettleInputSchema = z.strictObject({ + ...driverCommandIdentitySchema, claimToken: lowercaseUuidV4Schema, - commandId: nonEmptyStringSchema, - driverInstanceId: nonEmptyStringSchema, effectId: nonEmptyStringSchema, settlement: driverExternalToolEffectSettlementSchema, }); @@ -336,13 +331,11 @@ const driverEventBatchInputSchema = z.strictObject({ events: z.array(parsedSchema(parseDriverEventEnvelope)).max(driverRpcBatchMaxSize), }); -const driverEventReceiptSchema = z - .strictObject({ - eventId: nonEmptyStringSchema, - seq: nonNegativeSafeIntegerSchema("Driver event receipt seq"), - type: z.enum(RUNTIME_EVENT_KINDS), - }) - .transform(omitUndefined); +const driverEventReceiptSchema = z.strictObject({ + eventId: nonEmptyStringSchema, + seq: nonNegativeSafeIntegerSchema("Driver event receipt seq"), + type: z.enum(RUNTIME_EVENT_KINDS), +}); const driverEventBatchOutputSchema = z.strictObject({ accepted: z.array(driverEventReceiptSchema), diff --git a/src/runtimes/acp/acp-assistant-transcript-state.ts b/src/runtimes/acp/acp-assistant-transcript-state.ts index 8254f04..b3d15dd 100644 --- a/src/runtimes/acp/acp-assistant-transcript-state.ts +++ b/src/runtimes/acp/acp-assistant-transcript-state.ts @@ -141,7 +141,11 @@ export class AcpAssistantTranscriptState { }; } - completePrompt(stopReason: StopReason, usage: unknown): DriverEventInput[] { + completePrompt( + stopReason: StopReason, + usage: unknown, + requestedBy: "provider" | "user" = "user", + ): DriverEventInput[] { const events: DriverEventInput[] = []; const runId = this.#requireRunId(); const terminalError = @@ -204,7 +208,7 @@ export class AcpAssistantTranscriptState { events.push({ kind: "run.cancelled", payload: { - requestedBy: "user", + requestedBy, stopReason: "cancelled", }, runId, diff --git a/src/runtimes/acp/acp-driver-backend.ts b/src/runtimes/acp/acp-driver-backend.ts index 978d53e..3823b1e 100644 --- a/src/runtimes/acp/acp-driver-backend.ts +++ b/src/runtimes/acp/acp-driver-backend.ts @@ -96,10 +96,10 @@ export class AcpDriverBackend implements AgentDriverBackend { this.#runtimeBootstrapText = buildRuntimeBootstrapText(payload.execution); this.#turnController = new AcpTurnController( (context, reason, events) => this.#push(context, reason, events), - (context, providerPromptAdmitted) => - this.#settleCancelledTurn(context, providerPromptAdmitted), - (context, reason, closures, terminal) => - this.#eventPublisher.pushTerminal(context, reason, closures, terminal), + (context, providerPromptAdmitted, resumeSignal) => + this.#settleCancelledTurn(context, providerPromptAdmitted, resumeSignal), + (context, reason, closures, terminal, cancellationSignal) => + this.#eventPublisher.pushTerminal(context, reason, closures, terminal, cancellationSignal), ); this.#clientRequests = new AcpClientRequestHandler({ allowedRoots: payload.execution.session.additionalDirectories, @@ -503,11 +503,15 @@ export class AcpDriverBackend implements AgentDriverBackend { async #settleCancelledTurn( context: AgentDriverContext, providerPromptAdmitted: boolean, + resumeSignal: AbortSignal, ): Promise { if (this.#stopRequested) { await this.#joinStop(); return; } + if (resumeSignal.aborted) { + return; + } if (!providerPromptAdmitted) { return; } @@ -528,15 +532,20 @@ export class AcpDriverBackend implements AgentDriverBackend { await this.#joinStop(); return; } - + if (resumeSignal.aborted) { + return; + } try { await this.#connect( context, - AbortSignal.timeout(this.#remainingStopMs(deadline)), + AbortSignal.any([AbortSignal.timeout(this.#remainingStopMs(deadline)), resumeSignal]), false, sessionId, ); } catch (error) { + if (resumeSignal.aborted) { + return; + } if (!this.#stopRequested) { throw error; } diff --git a/src/runtimes/acp/acp-turn-controller.ts b/src/runtimes/acp/acp-turn-controller.ts index bb6e278..19f53c3 100644 --- a/src/runtimes/acp/acp-turn-controller.ts +++ b/src/runtimes/acp/acp-turn-controller.ts @@ -13,6 +13,7 @@ import { createDriverId } from "../../protocol/id"; import type { MessageId, RunId } from "../../protocol/id"; import type { RuntimeCommandInput } from "../../runtime-command"; import { raceWithAbort } from "../../utils/async"; +import { DriverCompletedTerminalSupersededError } from "../driver-event-publisher"; import type { AcpClientRequestHandler } from "./acp-client-request-handler"; import { toRequestMeta } from "./acp-configuration"; import { AcpAssistantTranscriptState } from "./acp-assistant-transcript-state"; @@ -23,33 +24,43 @@ interface ActiveAcpTurn { cancellationBarrier: Promise | null; cancellationReason: string | null; cancellationRequest: Promise | null; - cancelRequested: boolean; + cancellationRequestedByHost: boolean; readonly drainCancellation: AbortController; drainDeadline: ReturnType | null; fatal: { readonly cleanup: Promise; readonly error: Error } | null; + readonly promptCancellation: AbortController; providerPromptAdmitted: boolean; promptResponseAccepted: boolean; - readonly providerPromptSettled: ReturnType>; + readonly resumeCancellation: AbortController; + resumeCancellationDetach: (() => void) | null; readonly runId: RunId; + readonly runSignal: AbortSignal | null; readonly settled: ReturnType>; readonly terminalTurn: number; terminalStarted: boolean; } -function createActiveTurn(runId: RunId, terminalTurn: number): ActiveAcpTurn { +function createActiveTurn( + runId: RunId, + terminalTurn: number, + runSignal?: AbortSignal, +): ActiveAcpTurn { return { cancellation: new AbortController(), cancellationBarrier: null, cancellationReason: null, cancellationRequest: null, - cancelRequested: false, + cancellationRequestedByHost: false, drainCancellation: new AbortController(), drainDeadline: null, fatal: null, + promptCancellation: new AbortController(), providerPromptAdmitted: false, promptResponseAccepted: false, - providerPromptSettled: Promise.withResolvers(), + resumeCancellation: new AbortController(), + resumeCancellationDetach: null, runId, + runSignal: runSignal ?? null, settled: Promise.withResolvers(), terminalStarted: false, terminalTurn, @@ -89,15 +100,27 @@ function startDrainDeadline(active: ActiveAcpTurn): void { ); } -function requestCancellation(active: ActiveAcpTurn, reason: string): void { - if (active.cancelRequested || active.fatal !== null || active.terminalStarted) { +function requestCancellation( + active: ActiveAcpTurn, + reason: string, + allowTerminalStarted = false, + interruptPrompt = true, +): void { + if ( + active.cancellationReason !== null || + active.fatal !== null || + (active.terminalStarted && !allowTerminalStarted) + ) { return; } - active.cancelRequested = true; active.cancellationReason = reason; startDrainDeadline(active); - active.cancellation.abort(new DriverTurnCancelledError(reason)); + const cancellation = new DriverTurnCancelledError(reason); + active.cancellation.abort(cancellation); + if (interruptPrompt) { + active.promptCancellation.abort(cancellation); + } } export type AcpTurnEventPush = ( @@ -111,11 +134,13 @@ export type AcpTurnTerminalPush = ( reason: string, closures: readonly DriverEventInput[], terminal: DriverEventInput, + cancellationSignal?: AbortSignal, ) => Promise; export type AcpCancelledTurnBarrier = ( context: AgentDriverContext, providerPromptAdmitted: boolean, + resumeSignal: AbortSignal, ) => Promise; function parsePromptStopReason(value: unknown): StopReason { @@ -152,7 +177,7 @@ export class AcpTurnController { } isCancelling(): boolean { - return this.#active?.cancelRequested ?? false; + return this.#active !== null && this.#active.cancellationReason !== null; } activeSignal(): AbortSignal | undefined { @@ -161,7 +186,9 @@ export class AcpTurnController { abort(reason: string): void { if (this.#active !== null) { - requestCancellation(this.#active, reason); + this.#active.cancellationRequestedByHost = true; + this.#active.resumeCancellation.abort(); + requestCancellation(this.#active, reason, false, false); } } @@ -195,16 +222,32 @@ export class AcpTurnController { clientRequests.openFileWriteIngress(); const messageId = createDriverId() as MessageId; - const active = createActiveTurn(runId, clientRequests.beginTurnTerminals()); + const active = createActiveTurn(runId, clientRequests.beginTurnTerminals(), signal); this.#active = active; this.events.begin({ messageId, runId }); - const onAbort = () => + const onAbort = () => { + active.cancellationRequestedByHost = true; + const cancellation = + signal?.reason instanceof DriverTurnCancelledError ? signal.reason : null; + if (cancellation?.resumeAllowed) { + const preventResume = () => active.resumeCancellation.abort(); + cancellation.resumeSignal.addEventListener("abort", preventResume, { once: true }); + active.resumeCancellationDetach = () => + cancellation.resumeSignal.removeEventListener("abort", preventResume); + if (cancellation.resumeSignal.aborted) { + preventResume(); + } + } else { + active.resumeCancellation.abort(); + } requestCancellation( active, signal?.reason instanceof Error ? signal.reason.message : "ACP driver backend turn was cancelled.", + true, ); + }; signal?.addEventListener("abort", onAbort, { once: true }); if (signal?.aborted) { onAbort(); @@ -213,6 +256,7 @@ export class AcpTurnController { clientRequests.openTurnTranscriptIngress(); let drainTask: Promise | null = null; let preserveEventState = false; + let terminalDelivered = false; const drain = async () => { try { await (drainTask ??= drainTurnWork(clientRequests, active.drainCancellation.signal)); @@ -241,10 +285,18 @@ export class AcpTurnController { typeof reason === "string" ? reason : reason(events), events, ); + terminalDelivered = true; return events; } catch (error) { - restore(); - preserveEventState = true; + if (error === active.runSignal?.reason) { + restore(); + } else if ( + !(error instanceof DriverCompletedTerminalSupersededError) || + error.cause !== active.runSignal?.reason + ) { + restore(); + preserveEventState = true; + } throw error; } }; @@ -265,7 +317,7 @@ export class AcpTurnController { toPromptStartEvents({ messageId, runId, text: input.text }), ); - if (active.cancelRequested) { + if (active.cancellationReason !== null) { clientRequests.closeFileWriteIngress(); clientRequests.closePermissionIngress(); clientRequests.closeTurnTranscriptIngress(); @@ -284,31 +336,50 @@ export class AcpTurnController { } active.providerPromptAdmitted = true; - const promptResult = await connection.request(acpMethods.agent.session.prompt, { - _meta: { - ...toRequestMeta({ sessionContext: context.payload.execution.session.context }), - "mosoo.ai/messageId": messageId, - }, - prompt: [{ text: input.text, type: "text" }], - sessionId, - }); + const promptResult = await raceWithAbort( + connection.request(acpMethods.agent.session.prompt, { + _meta: { + ...toRequestMeta({ sessionContext: context.payload.execution.session.context }), + "mosoo.ai/messageId": messageId, + }, + prompt: [{ text: input.text, type: "text" }], + sessionId, + }), + active.promptCancellation.signal, + ); clientRequests.closeFileWriteIngress(); if (active.fatal !== null) { throw active.fatal.error; } const providerStopReason = parsePromptStopReason(promptResult.stopReason); active.promptResponseAccepted = true; - active.providerPromptSettled.resolve(true); clientRequests.closePermissionIngress(); clientRequests.closeTurnTranscriptIngress(); if (providerStopReason === "cancelled") { requestCancellation(active, "ACP provider cancelled the turn."); } await drain(); - const stopReason = active.cancelRequested ? "cancelled" : providerStopReason; - const promptCancelled = active.cancelRequested || providerStopReason === "cancelled"; + const stopReason = active.cancellationReason !== null ? "cancelled" : providerStopReason; + const promptCancelled = + active.cancellationReason !== null || providerStopReason === "cancelled"; + const cancelledByProvider = + providerStopReason === "cancelled" && !active.cancellationRequestedByHost; if (promptCancelled) { + if (active.cancellationRequestedByHost) { + await this.#publishCancellationRequest( + context, + active, + active.cancellationReason ?? "ACP driver backend turn was cancelled.", + ); + } await this.#crossCancelledTurnBarrier(context, active, clientRequests); + if (active.cancellationRequestedByHost) { + await this.#publishCancellationRequest( + context, + active, + active.cancellationReason ?? "ACP driver backend turn was cancelled.", + ); + } } active.terminalStarted = true; const completionEvents = await publishTerminal( @@ -318,7 +389,12 @@ export class AcpTurnController { : events.some((event) => event.kind === "run.failed") ? "driver.acp.prompt.failed" : "driver.acp.prompt.completed", - () => this.events.completePrompt(stopReason, promptResult.usage), + () => + this.events.completePrompt( + stopReason, + promptResult.usage, + cancelledByProvider ? "provider" : "user", + ), ); const promptFailed = completionEvents.some((event) => event.kind === "run.failed"); context.logger.info( @@ -334,7 +410,6 @@ export class AcpTurnController { throw new AcpPromptTerminalError(stopReason); } } catch (error) { - active.providerPromptSettled.resolve(active.providerPromptAdmitted && active.cancelRequested); clientRequests.closeFileWriteIngress(); clientRequests.closePermissionIngress(); clientRequests.closeTurnTranscriptIngress(); @@ -398,7 +473,10 @@ export class AcpTurnController { throw catchDrainError; } - if (error instanceof DriverTurnCancelledError || error instanceof AcpPromptTerminalError) { + if ( + error instanceof AcpPromptTerminalError || + (error instanceof DriverTurnCancelledError && terminalDelivered) + ) { throw error; } @@ -413,9 +491,23 @@ export class AcpTurnController { throw error; } - if (active.cancelRequested) { + if (active.cancellationReason !== null) { try { + if (active.cancellationRequestedByHost) { + await this.#publishCancellationRequest( + context, + active, + active.cancellationReason ?? "ACP driver backend turn was cancelled.", + ); + } await this.#crossCancelledTurnBarrier(context, active, clientRequests); + if (active.cancellationRequestedByHost) { + await this.#publishCancellationRequest( + context, + active, + active.cancellationReason ?? "ACP driver backend turn was cancelled.", + ); + } } catch (cleanupError) { active.terminalStarted = true; await publishTerminal("driver.acp.prompt.failed", () => @@ -450,7 +542,7 @@ export class AcpTurnController { clearTimeout(active.drainDeadline); } signal?.removeEventListener("abort", onAbort); - active.providerPromptSettled.resolve(false); + active.resumeCancellationDetach?.(); this.#active = null; if (!preserveEventState) { this.events.clear(); @@ -473,19 +565,12 @@ export class AcpTurnController { return; } + active.cancellationRequestedByHost = true; requestCancellation(active, "ACP driver backend turn was cancelled."); - const cancel = active.providerPromptAdmitted - ? connection.notify(acpMethods.agent.session.cancel, { sessionId }) - : Promise.resolve(); - const eventPush = this.#publishCancellationRequest(context, active, reason); - const [cancelResult, eventResult] = await Promise.allSettled([cancel, eventPush]); - - if (eventResult.status === "rejected") { - throw eventResult.reason; - } - if (cancelResult.status === "rejected" && !(await active.providerPromptSettled.promise)) { - throw cancelResult.reason; + if (active.providerPromptAdmitted) { + void connection.notify(acpMethods.agent.session.cancel, { sessionId }).catch(() => {}); } + void this.#publishCancellationRequest(context, active, reason).catch(() => {}); } #publishCancellationRequest( @@ -522,7 +607,13 @@ export class AcpTurnController { throw new Error("ACP terminal event batch must end with a run terminal."); } - await this.#pushTerminal(context, reason, events.slice(0, -1), terminal); + await this.#pushTerminal( + context, + reason, + events.slice(0, -1), + terminal, + terminal.kind === "run.completed" ? (this.#active?.runSignal ?? undefined) : undefined, + ); } async #crossCancelledTurnBarrier( @@ -533,7 +624,11 @@ export class AcpTurnController { try { await (active.cancellationBarrier ??= (async () => { await clientRequests.stopTurnTerminals(context, active.terminalTurn); - await this.#cancelledTurnBarrier(context, active.providerPromptAdmitted); + await this.#cancelledTurnBarrier( + context, + active.providerPromptAdmitted, + active.resumeCancellation.signal, + ); })()); } catch (error) { throw new DriverTurnCancellationCleanupError( diff --git a/src/runtimes/claude/agent-sdk-driver-backend.ts b/src/runtimes/claude/agent-sdk-driver-backend.ts index 0bf49a3..da57849 100644 --- a/src/runtimes/claude/agent-sdk-driver-backend.ts +++ b/src/runtimes/claude/agent-sdk-driver-backend.ts @@ -21,7 +21,10 @@ import type { DriverStartInput } from "../../protocol/start"; import type { RuntimeCommandInput } from "../../runtime-command"; import { raceWithAbort, settlePromiseWithTimeout } from "../../utils/async"; import type { AgentDriverBackend, AgentDriverContext } from "../../core/agent-driver-backend"; -import { DriverEventPublisher } from "../driver-event-publisher"; +import { + DriverCompletedTerminalSupersededError, + DriverEventPublisher, +} from "../driver-event-publisher"; import { toRuntimePublicId } from "../runtime-public-id"; import { computeRuntimeBootstrapDigest, writeSkillBootstrapArtifacts } from "../skill-bootstrap"; import { readProcessEnvString, toErrorMessage } from "./agent-sdk-json"; @@ -30,6 +33,7 @@ import { ClaudeAgentSdkPrewarm } from "./agent-sdk-prewarm"; import { ClaudeAgentSdkMessageTranslator, ClaudeTerminalWriteError, + type ClaudeTerminalOutcome, } from "./agent-sdk-message-translator"; import { CLAUDE_CODE_EXECUTABLE_ENV, @@ -42,13 +46,14 @@ import { drainClaudeTasks } from "./agent-sdk-tasks"; interface ActiveClaudeTurn { abortController: AbortController; - cancelTask: Promise | null; cancelReason: string | null; permissionTasks: Set>; processTasks: Set>; query: Query | null; queryCloseTask: Promise | null; runId: RunId; + runSignal: AbortSignal | null; + readonly settled: ReturnType>; state: "running" | "finalizing" | "cancelled"; } @@ -64,10 +69,17 @@ const DEFAULT_DEPENDENCIES: ClaudeAgentSdkDriverBackendDependencies = { startup, }; -const CLAUDE_INTERRUPT_TIMEOUT_MS = 1_500; +const CLAUDE_QUERY_RETURN_TIMEOUT_MS = 2_500; function isTurnCancelled(turn: ActiveClaudeTurn): boolean { - return turn.state === "cancelled"; + return turn.state === "cancelled" || turn.runSignal?.aborted === true; +} + +function turnCancellationReason(turn: ActiveClaudeTurn): string { + return ( + turn.cancelReason ?? + toErrorMessage(turn.runSignal?.reason, "Claude Agent SDK turn was cancelled.") + ); } export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { @@ -100,8 +112,16 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { this.#messageTranslator = new ClaudeAgentSdkMessageTranslator({ publicToolCallId: (nativeToolCallId) => toRuntimePublicId(nativeToolCallId, "claude-tool"), push: async (context, reason, events) => this.#push(context, reason, events), - pushTerminal: async (context, reason, closures, terminal) => - this.#eventPublisher.pushTerminal(context, reason, closures, terminal), + pushTerminal: async (context, reason, closures, terminal) => { + const activeTurn = this.#activeTurn; + await this.#eventPublisher.pushTerminal( + context, + reason, + closures, + terminal, + terminal.kind === "run.completed" ? (activeTurn?.runSignal ?? undefined) : undefined, + ); + }, recordNativeSessionId: async (context, sessionId) => this.#recordNativeSessionId(context, sessionId), replaceNativeSessionId: async (context, previousSessionId, nextSessionId) => @@ -160,6 +180,7 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { context: AgentDriverContext, input: RuntimeCommandInput, runId: RunId, + signal?: AbortSignal, ): Promise { if (this.#activeTurn) { throw new Error("Claude Agent SDK already has an active turn."); @@ -181,21 +202,28 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { const { abortController, permissionTasks, processTasks, warmQuery } = this.#prewarm.take(); const activeTurn: ActiveClaudeTurn = { abortController, - cancelTask: null, cancelReason: null, permissionTasks, processTasks, query: null, queryCloseTask: null, runId, + runSignal: signal ?? null, + settled: Promise.withResolvers(), state: "running", }; this.#activeTurn = activeTurn; + const turnSignal = + activeTurn.runSignal === null + ? activeTurn.abortController.signal + : AbortSignal.any([activeTurn.abortController.signal, activeTurn.runSignal]); let queryStartedAtMs = Date.now(); let queryOptionsMs = 0; let runStarted = false; + let terminalOutcome: ClaudeTerminalOutcome | null = null; + try { await this.#push(context, "driver.claude.turn.started", [ { @@ -218,16 +246,19 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { activeQuery = warmQuery.query(promptText); } else { const optionsStartedAtMs = Date.now(); - const queryOptions = await this.#dependencies.createQueryOptions({ - abortController, - context, - nativeSessionId: this.#nativeSessionId, - payload: this.#payload, - permissionTasks, - processTasks, - publicToolCallId: (nativeToolCallId) => - toRuntimePublicId(nativeToolCallId, "claude-tool"), - }); + const queryOptions = await raceWithAbort( + this.#dependencies.createQueryOptions({ + abortController, + context, + nativeSessionId: this.#nativeSessionId, + payload: this.#payload, + permissionTasks, + processTasks, + publicToolCallId: (nativeToolCallId) => + toRuntimePublicId(nativeToolCallId, "claude-tool"), + }), + turnSignal, + ); queryOptionsMs = Date.now() - optionsStartedAtMs; if (isTurnCancelled(activeTurn)) { @@ -242,7 +273,7 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { activeTurn.query = activeQuery; if (isTurnCancelled(activeTurn)) { - await this.#closeQuery(context, activeTurn, activeTurn.cancelReason ?? "turn.cancelled"); + await this.#closeQuery(context, activeTurn, turnCancellationReason(activeTurn)); throw new DriverTurnCancelledError("Claude Agent SDK turn was cancelled."); } } catch (error) { @@ -284,11 +315,15 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { nativeSessionIdPresent: Boolean(this.#nativeSessionId), }); - let completed = false; let firstProviderEventPublished = false; const providerStartedAtMs = Date.now(); + for (;;) { + const next = await raceWithAbort(activeQuery.next(), turnSignal); + if (next.done) { + break; + } + const message = next.value; - for await (const message of activeQuery) { if (isTurnCancelled(activeTurn)) { throw new DriverTurnCancelledError("Claude Agent SDK turn was cancelled."); } @@ -326,17 +361,17 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { await this.#closeQuery(context, activeTurn, "provider.result"); } - completed = await this.#messageTranslator.handleSdkMessage(context, message, runId); - if (completed) { + terminalOutcome = await this.#messageTranslator.handleSdkMessage(context, message, runId); + if (terminalOutcome !== null) { break; } } - if (isTurnCancelled(activeTurn)) { + if (terminalOutcome === null && isTurnCancelled(activeTurn)) { throw new DriverTurnCancelledError("Claude Agent SDK turn was cancelled."); } - if (!completed) { + if (terminalOutcome === null) { throw new Error("Claude Agent SDK query ended before a result frame."); } } catch (error) { @@ -344,14 +379,22 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { throw error; } + if (error instanceof ClaudeTerminalWriteError) { + const replaceableCompletion = + error.terminalKind === "run.completed" && + (error.cause === activeTurn.runSignal?.reason || + (error.cause instanceof DriverCompletedTerminalSupersededError && + error.cause.cause === activeTurn.runSignal?.reason)); + if (!replaceableCompletion) { + throw error.cause; + } + } + if (isTurnCancelled(activeTurn)) { - await this.#closeQuery(context, activeTurn, activeTurn.cancelReason ?? "turn.cancelled"); - await this.#messageTranslator.cancelTurn( - context, - runId, - activeTurn.cancelReason ?? "turn.cancelled", - ); - throw new DriverTurnCancelledError("Claude Agent SDK turn was cancelled."); + const cancellationReason = turnCancellationReason(activeTurn); + await this.#closeQuery(context, activeTurn, cancellationReason); + await this.#messageTranslator.cancelTurn(context, runId, cancellationReason); + throw new DriverTurnCancelledError(cancellationReason); } activeTurn.state = "finalizing"; @@ -377,8 +420,18 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { if (this.#activeTurn === activeTurn) { this.#activeTurn = null; } + activeTurn.settled.resolve(); } } + + if (terminalOutcome.kind === "run.cancelled") { + throw new DriverTurnCancelledError( + terminalOutcome.payload.reason ?? "Claude Agent SDK turn was cancelled by the provider.", + ); + } + if (terminalOutcome.kind === "run.failed") { + throw new Error(terminalOutcome.payload.error.message); + } } async cancelActiveTurn(context: AgentDriverContext, reason: string): Promise { @@ -388,13 +441,9 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { return; } - if (activeTurn.cancelTask !== null) { - await activeTurn.cancelTask; - return; - } - if (activeTurn.state === "finalizing") { - await this.#closeQuery(context, activeTurn, reason); + activeTurn.abortController.abort(reason); + void this.#closeQuery(context, activeTurn, reason).catch(() => {}); return; } @@ -404,33 +453,8 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { activeTurn.state = "cancelled"; activeTurn.cancelReason = reason; - - activeTurn.cancelTask = (async () => { - try { - if (activeTurn.query !== null) { - const interrupted = await settlePromiseWithTimeout( - Promise.resolve().then(() => activeTurn.query?.interrupt()), - { - label: "Claude Agent SDK turn interrupt", - timeoutMs: CLAUDE_INTERRUPT_TIMEOUT_MS, - }, - ); - - if (interrupted.status !== "completed") { - context.logger.debug("driver.claude.turn.interrupt_failed", { - message: toErrorMessage(interrupted.error, "Claude turn interrupt failed"), - reason, - runId: activeTurn.runId, - }); - } - } - } finally { - activeTurn.abortController.abort(reason); - await this.#closeQuery(context, activeTurn, reason); - } - })(); - - await activeTurn.cancelTask; + activeTurn.abortController.abort(reason); + void this.#closeQuery(context, activeTurn, reason).catch(() => {}); } stop(context: AgentDriverContext, reason: string, signal: AbortSignal): Promise { @@ -455,17 +479,27 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { ): Promise { const activeTurn = this.#activeTurn; const prewarmStop = this.#prewarm.stop(context, reason, signal); + const activeCleanup = + activeTurn === null + ? Promise.resolve() + : raceWithAbort( + (async () => { + await this.cancelActiveTurn(context, reason); + const [closeResult] = await Promise.allSettled([ + this.#closeQuery(context, activeTurn, reason), + activeTurn.settled.promise, + ]); + if (closeResult.status === "rejected") { + throw closeResult.reason; + } + })(), + signal, + ); const [activeResult, prewarmResult, pendingResult] = await Promise.allSettled([ - this.cancelActiveTurn(context, reason), + activeCleanup, prewarmStop, - drainClaudeTasks(this.#pendingProcessTasks), + raceWithAbort(drainClaudeTasks(this.#pendingProcessTasks), signal), ]); - if (activeTurn !== null) { - this.#retainProcessTasks(activeTurn); - if (activeResult.status === "rejected" && this.#activeTurn === activeTurn) { - this.#activeTurn = null; - } - } if (activeResult.status === "rejected") { throw activeResult.reason; @@ -545,19 +579,38 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { ): Promise { if (turn.queryCloseTask === null) { const query = turn.query; - turn.queryCloseTask = Promise.resolve() - .then(() => query?.return()) - .then( - () => {}, - (error) => { + turn.queryCloseTask = (async () => { + if (query !== null) { + try { + query.close(); + } catch (error) { + turn.abortController.abort(reason); context.logger.debug("driver.claude.turn.close_failed", { message: toErrorMessage(error, "query close failed"), reason, runId: turn.runId, }); - }, - ) - .then(() => drainClaudeTasks(turn.permissionTasks, turn.processTasks)); + } + + const returned = await settlePromiseWithTimeout( + Promise.resolve().then(() => query.return()), + { + label: "Claude Agent SDK query return", + timeoutMs: CLAUDE_QUERY_RETURN_TIMEOUT_MS, + }, + ); + if (returned.status !== "completed") { + turn.abortController.abort(reason); + context.logger.debug("driver.claude.turn.return_failed", { + message: toErrorMessage(returned.error, "query return failed"), + reason, + runId: turn.runId, + }); + } + } + + await drainClaudeTasks(turn.permissionTasks, turn.processTasks); + })(); } await turn.queryCloseTask; diff --git a/src/runtimes/claude/agent-sdk-event-writer.ts b/src/runtimes/claude/agent-sdk-event-writer.ts index 4497c28..00cf56e 100644 --- a/src/runtimes/claude/agent-sdk-event-writer.ts +++ b/src/runtimes/claude/agent-sdk-event-writer.ts @@ -42,6 +42,20 @@ interface ClaudeAgentSdkEventWriterOptions { push(context: AgentDriverContext, reason: string, events: DriverEventInput[]): Promise; } +export type ClaudeTerminalOutcome = + | (DriverEventInput & { + readonly kind: "run.cancelled"; + readonly payload: { readonly [key: string]: unknown; readonly reason: string }; + }) + | (DriverEventInput & { readonly kind: "run.completed" }) + | (DriverEventInput & { + readonly kind: "run.failed"; + readonly payload: { + readonly [key: string]: unknown; + readonly error: { readonly [key: string]: unknown; readonly message: string }; + }; + }); + export interface ClaudeToolStartEvent { context: AgentDriverContext; parentMessageId?: string; @@ -302,8 +316,8 @@ export class ClaudeAgentSdkEventWriter { message: string, retryable: boolean, details?: JsonObject, - ): DriverEventInput { - const event: DriverEventInput = { + ): Extract { + const event: Extract = { kind: "run.failed", payload: { error: { @@ -345,7 +359,7 @@ export class ClaudeAgentSdkEventWriter { runId: RunId, finalMessage: { readonly id: MessageId } | null, structuredOutput?: JsonValue, - ): DriverEventInput { + ): Extract { return { runId, kind: "run.completed", @@ -357,8 +371,11 @@ export class ClaudeAgentSdkEventWriter { }; } - runCancelled(runId: RunId, reason: string): DriverEventInput { - const event: DriverEventInput = { + runCancelled( + runId: RunId, + reason: string, + ): Extract { + const event: Extract = { kind: "run.cancelled", payload: { reason, stopReason: "cancelled" }, runId, diff --git a/src/runtimes/claude/agent-sdk-message-translator.ts b/src/runtimes/claude/agent-sdk-message-translator.ts index 452f8f3..eb5d77f 100644 --- a/src/runtimes/claude/agent-sdk-message-translator.ts +++ b/src/runtimes/claude/agent-sdk-message-translator.ts @@ -4,7 +4,12 @@ import { jsonValueSchema } from "../../contract"; import type { DriverEventInput } from "../../protocol/events"; import type { MessageId, RunId, SessionId } from "../../protocol/id"; import type { AgentDriverContext } from "../../core/agent-driver-backend"; -import { assertClaudeDurableEventFits, ClaudeAgentSdkEventWriter } from "./agent-sdk-event-writer"; +import { DriverCompletedTerminalSupersededError } from "../driver-event-publisher"; +import { + assertClaudeDurableEventFits, + ClaudeAgentSdkEventWriter, + type ClaudeTerminalOutcome, +} from "./agent-sdk-event-writer"; import { isRecord, readNumber, @@ -58,11 +63,15 @@ function exhaustSdkMessage(value: never): unknown { return value; } +export type { ClaudeTerminalOutcome } from "./agent-sdk-event-writer"; + export class ClaudeTerminalWriteError extends Error { override readonly name = "ClaudeTerminalWriteError"; + readonly terminalKind: ClaudeTerminalOutcome["kind"]; - constructor(cause: unknown) { + constructor(cause: unknown, terminalKind: ClaudeTerminalOutcome["kind"]) { super("Claude terminal delivery failed.", { cause }); + this.terminalKind = terminalKind; } } @@ -71,6 +80,7 @@ export class ClaudeAgentSdkMessageTranslator { readonly #options: ClaudeMessageTranslatorOptions; readonly #permissionDenialAdvisories = new Map(); readonly #state: ClaudeAgentSdkMessageState; + #turnClosureCommitted = false; constructor(options: ClaudeMessageTranslatorOptions) { this.#options = options; @@ -82,6 +92,7 @@ export class ClaudeAgentSdkMessageTranslator { this.#state.reset(); this.#events.resetTurnState(); this.#permissionDenialAdvisories.clear(); + this.#turnClosureCommitted = false; } async #cancelOpenTurn(context: AgentDriverContext): Promise { @@ -107,19 +118,36 @@ export class ClaudeAgentSdkMessageTranslator { terminal: DriverEventInput, reason: string, ): Promise { - const closure = this.#events.prepareTurnClosure(toolStatus); - await this.#options.pushTerminal( - context, - reason, - [...closure.events, claudeBackgroundTasksClosedEvent()], - terminal, - ); - closure.commit(); + const closure = this.#turnClosureCommitted ? null : this.#events.prepareTurnClosure(toolStatus); + const commit = () => { + if (closure === null) { + return; + } + closure.commit(); + this.#turnClosureCommitted = true; + this.#state.clearThoughtIds(); + for (const [messageId, runId] of this.#state.assistantMessages()) { + this.#state.completeAssistantMessage(runId, messageId, toolStatus === "completed"); + } + }; - this.#state.clearThoughtIds(); - for (const [messageId, runId] of this.#state.assistantMessages()) { - this.#state.completeAssistantMessage(runId, messageId, toolStatus === "completed"); + try { + await this.#options.pushTerminal( + context, + reason, + closure === null ? [] : [...closure.events, claudeBackgroundTasksClosedEvent()], + terminal, + ); + } catch (error) { + if ( + terminal.kind === "run.completed" && + error instanceof DriverCompletedTerminalSupersededError + ) { + commit(); + } + throw error; } + commit(); } async cancelTurn(context: AgentDriverContext, runId: RunId, reason: string): Promise { @@ -158,7 +186,7 @@ export class ClaudeAgentSdkMessageTranslator { context: AgentDriverContext, message: SDKMessage, runId: RunId, - ): Promise { + ): Promise { const sessionId = readClaudeSdkSessionId(message); if (sessionId !== null && message.type !== "conversation_reset") { @@ -168,37 +196,36 @@ export class ClaudeAgentSdkMessageTranslator { switch (message.type) { case "assistant": { await this.#handleAssistantMessage(context, message, runId); - return false; + return null; } case "auth_status": case "rate_limit_event": case "tool_progress": case "tool_use_summary": { await this.#events.pushDiagnostic(context, message); - return false; + return null; } case "conversation_reset": { await this.#handleConversationReset(context, message); - return false; + return null; } case "result": { - await this.#handleResultMessage(context, message, runId); - return true; + return this.#handleResultMessage(context, message, runId); } case "stream_event": { await this.#handleStreamEvent(context, message, runId); - return false; + return null; } case "system": { await this.#handleSystemMessage(context, message, runId); - return false; + return null; } case "user": { await this.#handleUserMessage(context, message, runId); - return false; + return null; } case "prompt_suggestion": { - return false; + return null; } default: { const unexpected = exhaustSdkMessage(message); @@ -207,7 +234,7 @@ export class ClaudeAgentSdkMessageTranslator { "driver.claude.message.unknown", isRecord(unexpected) ? unexpected : { value: String(unexpected) }, ); - return false; + return null; } } } @@ -771,7 +798,7 @@ export class ClaudeAgentSdkMessageTranslator { context: AgentDriverContext, message: Extract, runId: RunId, - ): Promise { + ): Promise { const successful = isClaudeResultSuccessful(message); const cancelled = isClaudeResultCancelled(message); @@ -816,23 +843,23 @@ export class ClaudeAgentSdkMessageTranslator { ); const finishResult = async ( toolStatus: "cancelled" | "completed" | "failed", - terminal: DriverEventInput, + terminal: ClaudeTerminalOutcome, reason: string, - ): Promise => { + ): Promise => { try { await this.finishTurnWithTerminal(context, toolStatus, terminal, reason); } catch (error) { - throw new ClaudeTerminalWriteError(error); + throw new ClaudeTerminalWriteError(error, terminal.kind); } + return terminal; }; if (cancelled) { - await finishResult( + return finishResult( "cancelled", this.#events.runCancelled(runId, message.terminal_reason ?? "provider.aborted"), "driver.claude.turn.cancelled", ); - return; } if (message.subtype === "success" && successful) { @@ -843,7 +870,7 @@ export class ClaudeAgentSdkMessageTranslator { : jsonValueSchema.safeParse(message.structured_output); if (structuredOutput !== undefined && !structuredOutput.success) { - await finishResult( + return finishResult( "failed", this.#events.runError( runId, @@ -853,7 +880,6 @@ export class ClaudeAgentSdkMessageTranslator { ), "driver.claude.turn.failed", ); - return; } const completedTerminal = this.#events.runFinished(runId, null, structuredOutput?.data); @@ -863,7 +889,7 @@ export class ClaudeAgentSdkMessageTranslator { MAX_CLAUDE_STRUCTURED_TERMINAL_BYTES; if (structuredOutputTooLarge) { - await finishResult( + return finishResult( "failed", this.#events.runError( runId, @@ -873,7 +899,6 @@ export class ClaudeAgentSdkMessageTranslator { ), "driver.claude.turn.failed", ); - return; } if ( @@ -897,15 +922,14 @@ export class ClaudeAgentSdkMessageTranslator { ? null : this.#state.resolveFinalAssistantSnapshot(runId, resultText); - await finishResult( + return finishResult( "completed", this.#events.runFinished(runId, finalMessage, structuredOutput?.data), "driver.claude.turn.completed", ); - return; } - await finishResult( + return finishResult( "failed", this.#events.runError( runId, diff --git a/src/runtimes/driver-event-publisher.ts b/src/runtimes/driver-event-publisher.ts index f942c5d..fb4fbf8 100644 --- a/src/runtimes/driver-event-publisher.ts +++ b/src/runtimes/driver-event-publisher.ts @@ -27,6 +27,7 @@ import { interface TerminalSettlement { readonly acceptedSourceEventIds: Set; readonly activeRunId: RunId; + readonly cancellationSignal: AbortSignal | null; readonly events: readonly DriverEventInput[]; readonly key: string; signal: AbortSignal; @@ -90,6 +91,13 @@ class QueuedPushDeliveryError extends Error { } } +export class DriverCompletedTerminalSupersededError extends Error { + constructor(reason: unknown) { + super("Driver completed terminal was superseded by cancellation.", { cause: reason }); + this.name = "DriverCompletedTerminalSupersededError"; + } +} + function deliveryCause(error: unknown): unknown { return error instanceof QueuedPushDeliveryError ? error.deliveryCause : error; } @@ -179,6 +187,7 @@ export class DriverEventPublisher { events: readonly DriverEventInput[], terminalSettlement?: TerminalSettlement, sessionScoped = false, + deliverySignal = terminalSettlement?.signal, ): Promise { if (events.length === 0) { this.#requestPendingDrain(context, reason); @@ -231,12 +240,7 @@ export class DriverEventPublisher { this.#pendingTerminalKey !== null && terminalDriverEventRetryKey(events, activeRunId) === this.#pendingTerminalKey ) { - const retry = this.#createPendingRetry( - context, - reason, - undefined, - terminalSettlement?.signal, - ); + const retry = this.#createPendingRetry(context, reason, undefined, deliverySignal); this.#enqueue(retry); return retry.promise; } @@ -261,7 +265,7 @@ export class DriverEventPublisher { const entry = adoption.events.length === 0 ? null - : this.#admit(context, reason, adoption.events, activeRunId, terminalSettlement?.signal); + : this.#admit(context, reason, adoption.events, activeRunId, deliverySignal); if (entry === null && adoption.sourceEventIds.length === 0) { this.#requestPendingDrain(context, reason); @@ -299,7 +303,7 @@ export class DriverEventPublisher { context, reason, adoption.sourceEventIds, - terminalSettlement?.signal, + deliverySignal, ); this.#enqueue(retry); deliveries.push(Promise.race([acknowledged, retry.promise.then(() => acknowledged)])); @@ -379,6 +383,7 @@ export class DriverEventPublisher { reason: string, closures: readonly DriverEventInput[], terminal: DriverEventInput, + cancellationSignal?: AbortSignal, ): Promise { if (!isRunTerminalDriverEvent(terminal)) { throw new Error("Driver terminal push requires a run terminal event."); @@ -387,6 +392,9 @@ export class DriverEventPublisher { if (!isLosslessDriverEvent(terminal)) { throw new Error("Driver terminal push requires lossless events."); } + if (cancellationSignal !== undefined && terminal.kind !== "run.completed") { + throw new Error("Only a completed driver terminal can lose to cancellation."); + } for (const closure of closures) { if (!isLosslessDriverEvent(closure)) { @@ -441,6 +449,8 @@ export class DriverEventPublisher { this.#terminalSettlement = null; } + cancellationSignal?.throwIfAborted(); + const retryCandidates = [ ...(this.#inFlightEvents ?? this.#pendingEvents), ...this.#queue.flatMap((entry) => entry.events), @@ -511,6 +521,7 @@ export class DriverEventPublisher { .filter((sourceEventId) => this.#acceptedInFlightSourceEventIds.has(sourceEventId)), ), activeRunId, + cancellationSignal: cancellationSignal ?? null, events, key, nextIndex: 0, @@ -535,6 +546,13 @@ export class DriverEventPublisher { if (settlement.task === task) { settlement.task = null; } + if ( + settlement.cancellationSignal?.aborted === true && + settlement.nextIndex === settlement.events.length - 1 + ) { + this.#abandonCancelledTerminalSettlement(settlement); + throw new DriverCompletedTerminalSupersededError(settlement.cancellationSignal.reason); + } throw error; }, @@ -565,6 +583,10 @@ export class DriverEventPublisher { const event = settlement.events[settlement.nextIndex]!; const deliveryReason = terminal ? reason : `${reason}.items`; const retryReason = `${deliveryReason}.retry`; + const deliverySignal = + terminal && settlement.cancellationSignal !== null + ? AbortSignal.any([settlement.signal, settlement.cancellationSignal]) + : settlement.signal; let delivery: Promise; try { @@ -575,12 +597,12 @@ export class DriverEventPublisher { ); delivery = retained === undefined - ? this.#submit(context, deliveryReason, [event], settlement) + ? this.#submit(context, deliveryReason, [event], settlement, false, deliverySignal) : this.#retryPending( context, retryReason, [retained.event.sourceEventId!], - settlement.signal, + deliverySignal, ); } catch (error) { this.#requestPendingDrain(context, deliveryReason); @@ -601,7 +623,7 @@ export class DriverEventPublisher { context, retryReason, error.retainedSourceEventIds, - settlement.signal, + deliverySignal, ); } } @@ -1038,4 +1060,43 @@ export class DriverEventPublisher { settlement.nextIndex += 1; } } + + #abandonCancelledTerminalSettlement(settlement: TerminalSettlement): void { + if (this.#terminalSettlement !== settlement) { + return; + } + if ( + settlement.task !== null || + settlement.nextIndex !== settlement.events.length - 1 || + settlement.acceptedSourceEventIds.size > 0 + ) { + throw new Error("Cannot abandon a selected driver run terminal."); + } + + const sourceEventId = settlement.events.at(-1)!.sourceEventId!; + if ( + this.#inFlightEvents?.some(({ event }) => event.sourceEventId === sourceEventId) === true || + this.#queue.some((entry) => + entry.events.some(({ event }) => event.sourceEventId === sourceEventId), + ) + ) { + throw new Error("Cannot abandon an in-flight driver terminal batch."); + } + + const pending = this.#pendingEvents.filter( + ({ event }) => event.sourceEventId !== sourceEventId, + ); + if (pending.some(({ terminalLane }) => terminalLane)) { + throw new Error("Cannot abandon a driver terminal while another terminal is pending."); + } + + this.#eventSettlements + .get(sourceEventId) + ?.reject( + settlement.cancellationSignal?.reason ?? new Error("Driver terminal was cancelled."), + ); + this.#eventSettlements.delete(sourceEventId); + this.#setPending(pending, null); + this.#terminalSettlement = null; + } } diff --git a/src/runtimes/mcp/remote-http-mcp-executor.ts b/src/runtimes/mcp/remote-http-mcp-executor.ts index 0aa1cc6..e3f169d 100644 --- a/src/runtimes/mcp/remote-http-mcp-executor.ts +++ b/src/runtimes/mcp/remote-http-mcp-executor.ts @@ -138,13 +138,11 @@ function normalizeCallToolResult( result: CallToolResult, command: McpExecuteCommand, ): McpExternalToolExecutionResult { - const textContent = result.content.flatMap((block) => - block.type === "text" ? [block.text] : [], - ); + const content = result.content[0]; const plainText = result.content.length === 1 && - result.content[0]?.type === "text" && - Object.keys(result.content[0]).every((key) => key === "type" || key === "text"); + content?.type === "text" && + Object.keys(content).every((key) => key === "type" || key === "text"); const hasRichContent = !plainText || result.structuredContent !== undefined; const outputText = hasRichContent @@ -159,11 +157,7 @@ function normalizeCallToolResult( null, 2, ) - : textContent.length > 0 - ? textContent.join("\n\n") - : result.isError === true - ? `MCP tool ${command.toolName} reported an error without textual details.` - : ""; + : content.text; return { ...(result.isError === undefined ? {} : { isError: result.isError }), diff --git a/src/runtimes/openai/app-server-client.ts b/src/runtimes/openai/app-server-client.ts index 448cd16..a4ae68c 100644 --- a/src/runtimes/openai/app-server-client.ts +++ b/src/runtimes/openai/app-server-client.ts @@ -37,7 +37,6 @@ import type { ClientRequestResult, RequestId, ServerNotificationMethod, - ServerNotificationParams, } from "./app-server-protocol"; import { CLIENT_RESULT_SCHEMAS, @@ -55,8 +54,10 @@ interface PendingJsonRpcRequest { accept: ((value: unknown) => Promise) | null; method: string; parse(value: unknown): unknown; + receiveAtWire: ((value: unknown) => void) | null; reject(error: Error): void; rejectAtBarrier: ((error: Error) => Promise) | null; + releaseAtWire: (() => void) | null; responseQueued: boolean; resolve(value: unknown): void; } @@ -73,10 +74,7 @@ interface OpenAiAppServerClientStartResult { type OpenAiRuntimeHomeState = Awaited>; interface OpenAiClientContext extends AgentDriverContext { - handleNotification( - method: M, - params: ServerNotificationParams[M], - ): Promise; + handleNotification(method: ServerNotificationMethod, params: JsonObject): Promise; handleProtocolError(error: Error): Promise; mapToolCallId(toolCallId: string): string; } @@ -568,7 +566,9 @@ export class OpenAiAppServerClient { params: ClientRequestParams[M], handlers: { accept(result: ClientRequestResult[M]): Promise; + received(result: ClientRequestResult[M]): void; reject(error: Error): Promise; + released(): void; }, signal?: AbortSignal, ): Promise { @@ -579,7 +579,12 @@ export class OpenAiAppServerClient { method: string, params: unknown, parseResult: (value: unknown) => T, - handlers: { accept(result: T): Promise; reject(error: Error): Promise } | null, + handlers: { + accept(result: T): Promise; + received(result: T): void; + reject(error: Error): Promise; + released(): void; + } | null, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); @@ -587,15 +592,18 @@ export class OpenAiAppServerClient { this.#nextId += 1; const response = Promise.withResolvers(); - this.#pendingRequests.set(id, { + const pending: PendingJsonRpcRequest = { accept: handlers === null ? null : async (value: unknown) => handlers.accept(value as T), method, parse: parseResult, + receiveAtWire: handlers === null ? null : (value: unknown) => handlers.received(value as T), reject: response.reject, rejectAtBarrier: handlers?.reject ?? null, + releaseAtWire: handlers?.released ?? null, responseQueued: false, resolve: response.resolve, - }); + }; + this.#pendingRequests.set(id, pending); try { this.#send({ @@ -604,6 +612,7 @@ export class OpenAiAppServerClient { ...(params === undefined ? {} : { params }), }); } catch (error) { + this.#releaseWireBarrier(pending); this.#pendingRequests.delete(id); throw error; } @@ -620,11 +629,13 @@ export class OpenAiAppServerClient { } if (result.status === "timed_out") { + this.#releaseWireBarrier(pending); await this.#failProtocol(result.error); } throw result.error; } finally { + this.#releaseWireBarrier(pending); this.#pendingRequests.delete(id); } } @@ -937,6 +948,7 @@ export class OpenAiAppServerClient { return this.#protocolFailureCommit ?? Promise.resolve(); } + this.#releasePendingWireBarriers(); this.#fatalError = error; this.#process?.stdout.pause(); const barrier = this.#serverMessageQueue; @@ -1055,6 +1067,19 @@ export class OpenAiAppServerClient { return; } pending.responseQueued = true; + if ("error" in parsed.data) { + this.#releaseWireBarrier(pending); + } else { + try { + this.#receiveWireResult(pending, pending.parse(parsed.data.result)); + } catch (error) { + const failure = + error instanceof Error ? error : new Error("OpenAi app-server result parse failed."); + this.#releaseWireBarrier(pending); + void this.#failProtocol(failure); + return; + } + } this.#serverMessageQueue = this.#processResponse( this.#serverMessageQueue, id, @@ -1076,6 +1101,9 @@ export class OpenAiAppServerClient { ): Promise { await previousMessage; + if (this.#stopRequested) { + return; + } if (this.#pendingRequests.get(id) !== pending) { return; } @@ -1229,9 +1257,37 @@ export class OpenAiAppServerClient { #rejectPending(error: Error): void { for (const pending of this.#pendingRequests.values()) { + this.#releaseWireBarrier(pending); pending.reject(error); } this.#pendingRequests.clear(); } + + #receiveWireResult(pending: PendingJsonRpcRequest, result: unknown): void { + const receive = pending.receiveAtWire; + const release = pending.releaseAtWire; + pending.receiveAtWire = null; + pending.releaseAtWire = null; + + try { + receive?.(result); + } catch (error) { + release?.(); + throw error; + } + } + + #releaseWireBarrier(pending: PendingJsonRpcRequest): void { + const release = pending.releaseAtWire; + pending.receiveAtWire = null; + pending.releaseAtWire = null; + release?.(); + } + + #releasePendingWireBarriers(): void { + for (const pending of this.#pendingRequests.values()) { + this.#releaseWireBarrier(pending); + } + } } diff --git a/src/runtimes/openai/app-server-driver-backend.ts b/src/runtimes/openai/app-server-driver-backend.ts index 2d6fb08..47aaf68 100644 --- a/src/runtimes/openai/app-server-driver-backend.ts +++ b/src/runtimes/openai/app-server-driver-backend.ts @@ -35,7 +35,6 @@ import type { ThreadStartParams, ThreadStartResponse, TurnStatus, - TurnStartParams, } from "./app-server-protocol"; /** @@ -52,14 +51,6 @@ function resolveApprovalPolicy(payload: DriverStartInput): ApprovalPolicy { return isDriverFullAccess(payload) ? "never" : "untrusted"; } -interface OpenAiTurnStartInput { - readonly approvalPolicy: ApprovalPolicy; - readonly cwd: string; - readonly model: string; - readonly text: string; - readonly threadId: string; -} - interface OpenAiPhaseMeasure { (name: string, task: () => Promise): Promise; } @@ -143,26 +134,11 @@ function toRecoveryItems( })); } -function createTurnParams(input: OpenAiTurnStartInput): TurnStartParams { - return { - approvalPolicy: input.approvalPolicy, - cwd: input.cwd, - input: [ - { - text: input.text, - text_elements: [], - type: "text", - }, - ], - model: input.model, - threadId: input.threadId, - }; -} - export class OpenAiAppServerDriverBackend implements AgentDriverBackend { readonly runtime: DriverRuntime = "openai-runtime"; readonly #payload: DriverStartInput; readonly #eventPublisher = new DriverEventPublisher(this.runtime, () => this.#threadId); + #activeCancellationTask: Promise | null = null; #client: OpenAiAppServerClient | null = null; #clientStartupCancellation: AbortController | null = null; #clientStopRequested = false; @@ -171,6 +147,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { #pendingTurnStartCancellationReason: string | null = null; #pendingTurnStartServerRequests: Promise | null = null; #pendingTurnStartUpdates: Promise | null = null; + #pendingCancellationSettlement: Promise | null = null; #restartThreadId: string | null = null; #stopping = false; #threadId: string | null = null; @@ -194,8 +171,8 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { push: async (context, reason, events) => this.#eventPublisher.push(context, reason, events), pushSession: async (context, reason, events) => this.#eventPublisher.pushSession(context, reason, events), - pushTerminal: async (context, reason, closures, terminal) => - this.#eventPublisher.pushTerminal(context, reason, closures, terminal), + pushTerminal: async (context, reason, closures, terminal, cancellationSignal) => + this.#eventPublisher.pushTerminal(context, reason, closures, terminal, cancellationSignal), requireThreadId: () => this.#requireThreadId(), }); @@ -302,6 +279,10 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { if (await this.#events.failActiveTurns(context, error)) { return; } + if (this.#events.hasAdmittedTerminalTurn()) { + context.lifecycle.fail(boundedError); + return; + } if (await this.#failTurnStart(context, error)) { return; } @@ -519,6 +500,7 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { context: AgentDriverContext, input: RuntimeCommandInput, runId: RunId, + signal?: AbortSignal, ): Promise { this.#turnStartInFlight = true; this.#turnStartRunId = runId; @@ -529,6 +511,9 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { this.#pendingTurnStartUpdates = null; let completion: Promise; + let turnAdmission: ReturnType | null = + this.#events.beginTurnAdmission(runId, signal); + const admission = turnAdmission; try { await this.#ensureClient(context); @@ -545,76 +530,135 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { }); const turnStartRequestedAtMs = Date.now(); + this.#events.armTurnAdmission(admission); const accepted = await client.requestAtWireBarrier( "turn/start", - createTurnParams({ + { approvalPolicy: resolveApprovalPolicy(this.#payload), cwd: this.#payload.execution.session.cwd, + input: [ + { + text: input.text, + text_elements: [], + type: "text", + }, + ], model: this.#payload.execution.model, - text: input.text, threadId, - }), + }, { accept: async (turnResult) => { const turnId = turnResult.turn.id; - const publicTurnId = this.#events.publicTurnId(turnId); - const turnCompletion = this.#events.trackTurn(turnId, runId); + const terminalObserved = this.#events.hasTerminalTurn(turnId); + const turnCompletion = this.#events.claimTurnAdmission( + admission, + turnId, + runId, + signal, + ); + turnAdmission = null; void turnCompletion.catch(() => {}); this.#claimTurnStartResponse(); - const turnStartedAtMs = Date.now(); - await this.#eventPublisher.push(context, "driver.openai.provider.turn_start", [ - createTimingEvent({ - completedAt: new Date(turnStartedAtMs).toISOString(), - path: "unknown", - phases: [ - createTimingPhase( - "provider.turn_start", - toDurationMs(turnStartRequestedAtMs, turnStartedAtMs), - ), - ], - runId, - sessionId: context.payload.execution.run.sessionId, - stage: "driver_turn", - startedAt: new Date(turnStartRequestedAtMs).toISOString(), - native: { - eventName: "provider.turn_start", - provider: "openai", - turnId: publicTurnId, - }, - }), - ]); - - await this.#events.publishRunStarted(context, { runId, turnId }); - - if (isTerminalTurn(turnResult.turn.status)) { - await this.#events.handleNotification(context, "turn/completed", { - threadId, - turn: turnResult.turn, - }); + if (!terminalObserved) { + const publicTurnId = this.#events.publicTurnId(turnId); + const turnStartedAtMs = Date.now(); + await this.#eventPublisher.push(context, "driver.openai.provider.turn_start", [ + createTimingEvent({ + completedAt: new Date(turnStartedAtMs).toISOString(), + path: "unknown", + phases: [ + createTimingPhase( + "provider.turn_start", + toDurationMs(turnStartRequestedAtMs, turnStartedAtMs), + ), + ], + runId, + sessionId: context.payload.execution.run.sessionId, + stage: "driver_turn", + startedAt: new Date(turnStartRequestedAtMs).toISOString(), + native: { + eventName: "provider.turn_start", + provider: "openai", + turnId: publicTurnId, + }, + }), + ]); + + await this.#events.publishRunStarted(context, { runId, turnId }); + + if (isTerminalTurn(turnResult.turn.status)) { + await this.#events.handleNotification(context, "turn/completed", { + threadId, + turn: turnResult.turn, + }); + } } return { completion: turnCompletion }; }, + received: (turnResult) => { + this.#events.bindTurnAdmission(admission, turnResult.turn.id); + }, reject: async (error) => { this.#claimTurnStartResponse(); await this.#publishTurnStartFailure(context, runId, error); }, + released: () => { + this.#events.releaseTurnAdmissionSelection(admission); + }, }, ); completion = accepted.completion; } catch (error) { - const publishCancellation = this.#pendingTurnStartCancellationEvent; - const pendingCancellationReason = this.#pendingTurnStartCancellationReason; + const signalCancellation = + signal?.aborted === true && signal.reason instanceof DriverTurnCancelledError + ? signal.reason + : null; + const pendingCancellationReason = + this.#pendingTurnStartCancellationReason ?? signalCancellation?.message ?? null; + const publishCancellation = + this.#pendingTurnStartCancellationReason === null + ? (signalCancellation?.resumeAllowed ?? false) + : this.#pendingTurnStartCancellationEvent; const failure = error instanceof Error ? error : new Error("OpenAI turn start failed."); if (pendingCancellationReason !== null) { - await this.#finishPendingTurnStartCancellation( - context, - runId, - pendingCancellationReason, - publishCancellation, - ); + await this.#finishPendingTurnStartCancellation(); + const admittedTurnId = + turnAdmission === null ? null : this.#events.admittedTurnId(turnAdmission); + + try { + if (turnAdmission !== null && admittedTurnId !== null) { + const turnCompletion = this.#events.claimTurnAdmission( + turnAdmission, + admittedTurnId, + runId, + signal, + ); + turnAdmission = null; + void turnCompletion.catch(() => {}); + if (publishCancellation) { + await this.#events.cancelTurn(context, admittedTurnId, pendingCancellationReason); + } else { + this.#events.rejectTurn( + admittedTurnId, + new DriverTurnCancelledError(pendingCancellationReason), + ); + } + } else if (publishCancellation) { + await this.#publishTurnStartCancellation(context, runId, pendingCancellationReason); + } + } catch (cancellationError) { + throw new DriverTurnCancellationCleanupError( + `OpenAI pending turn cancellation settlement failed: ${ + cancellationError instanceof Error + ? cancellationError.message + : "unknown cancellation error" + }`, + cancellationError, + ); + } this.#events.releaseTurnState(); this.#turnStartInFlight = false; this.#turnStartRunId = null; @@ -623,6 +667,24 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { throw new DriverTurnCancelledError(pendingCancellationReason); } + const admittedTurnId = + turnAdmission === null ? null : this.#events.admittedTurnId(turnAdmission); + if ( + turnAdmission !== null && + admittedTurnId !== null && + this.#events.hasTerminalTurn(admittedTurnId) + ) { + const terminalCompletion = this.#events.claimTurnAdmission( + turnAdmission, + admittedTurnId, + runId, + signal, + ); + turnAdmission = null; + this.#claimTurnStartResponse(); + return await terminalCompletion; + } + try { await this.#failTurnStart(context, failure); } finally { @@ -630,6 +692,10 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { this.#turnStartRunId = null; } throw new Error(toOpenAiProtocolError({ message: failure.message }).message); + } finally { + if (turnAdmission !== null) { + this.#events.releaseTurnAdmission(turnAdmission); + } } await completion; @@ -791,18 +857,23 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { ); } - async #finishPendingTurnStartCancellation( - context: AgentDriverContext, - runId: RunId, - reason: string, - publish: boolean, - ): Promise { + async #finishPendingTurnStartCancellation(): Promise { try { - await this.#waitPendingTurnStartCleanup(); - await this.#waitPendingTurnStartServerRequests(); - await this.#waitPendingTurnStartUpdates(); - if (publish) { - await this.#publishTurnStartCancellation(context, runId, reason); + let pending = this.#pendingTurnStartCleanup; + this.#pendingTurnStartCleanup = null; + if (pending !== null) { + await pending; + } + pending = this.#pendingTurnStartServerRequests; + this.#pendingTurnStartServerRequests = null; + if (pending !== null) { + await pending; + } + + pending = this.#pendingTurnStartUpdates; + this.#pendingTurnStartUpdates = null; + if (pending !== null) { + await pending; } } catch (error) { throw new DriverTurnCancellationCleanupError( @@ -814,45 +885,39 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { } } - async #waitPendingTurnStartServerRequests(): Promise { - const pending = this.#pendingTurnStartServerRequests; - this.#pendingTurnStartServerRequests = null; - if (pending !== null) { - await pending; - } - } - - async #waitPendingTurnStartCleanup(): Promise { - const pending = this.#pendingTurnStartCleanup; - this.#pendingTurnStartCleanup = null; - if (pending !== null) { - await pending; - } + cancelActiveTurn(context: AgentDriverContext, reason: string): Promise { + return this.#startCancellation(context, reason, true); } - async #waitPendingTurnStartUpdates(): Promise { - const pending = this.#pendingTurnStartUpdates; - this.#pendingTurnStartUpdates = null; - if (pending !== null) { - await pending; + #startCancellation( + context: AgentDriverContext, + reason: string, + publishTurnStartCancellation: boolean, + ): Promise { + if (this.#activeCancellationTask !== null) { + return this.#activeCancellationTask; } - } - async cancelActiveTurn(context: AgentDriverContext, reason: string): Promise { - try { - await this.#cancelActiveTurn(context, reason, true); - } catch (error) { - if (error instanceof DriverTurnCancellationCleanupError) { - throw error; - } + const task = this.#cancelActiveTurn(context, reason, publishTurnStartCancellation) + .catch((error: unknown) => { + if (error instanceof DriverTurnCancellationCleanupError) { + throw error; + } - throw new DriverTurnCancellationCleanupError( - `OpenAI cancelled turn cleanup failed: ${ - error instanceof Error ? error.message : "unknown cleanup error" - }`, - error, - ); - } + throw new DriverTurnCancellationCleanupError( + `OpenAI cancelled turn cleanup failed: ${ + error instanceof Error ? error.message : "unknown cleanup error" + }`, + error, + ); + }) + .finally(() => { + if (this.#activeCancellationTask === task) { + this.#activeCancellationTask = null; + } + }); + this.#activeCancellationTask = task; + return task; } async #cancelActiveTurn( @@ -943,7 +1008,8 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { activeTurnIds.map((turnId) => this.#events.cancelTurn(context, turnId, reason, () => updatesDrained), ), - ).catch((error: unknown) => { + ).then(() => undefined); + const cancellationSettlementTask = cancellationEvents.catch((error: unknown) => { const cleanupError = new DriverTurnCancellationCleanupError( `OpenAI cancellation event delivery failed: ${ error instanceof Error ? error.message : "unknown delivery error" @@ -953,8 +1019,9 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { this.#events.rejectActiveTurns(cleanupError); throw cleanupError; }); - void cancellationEvents.catch(() => {}); - const cancellationSettlement = await settlePromiseWithTimeout(cancellationEvents, { + this.#pendingCancellationSettlement = cancellationSettlementTask; + void cancellationSettlementTask.catch(() => {}); + const cancellationSettlement = await settlePromiseWithTimeout(cancellationSettlementTask, { label: "OpenAI turn cancellation event delivery", timeoutMs: OPENAI_TURN_CANCEL_EVENT_TIMEOUT_MS, }); @@ -1019,8 +1086,20 @@ export class OpenAiAppServerDriverBackend implements AgentDriverBackend { try { signal.throwIfAborted(); - if (!this.#clientStopRequested) { - await raceWithAbort(this.#cancelActiveTurn(context, reason, false), signal); + if (this.#activeCancellationTask !== null) { + await this.#activeCancellationTask; + } else if (!this.#clientStopRequested) { + await this.#startCancellation(context, reason, false); + } + const cancellationSettlement = this.#pendingCancellationSettlement; + if (cancellationSettlement !== null) { + try { + await cancellationSettlement; + } finally { + if (this.#pendingCancellationSettlement === cancellationSettlement) { + this.#pendingCancellationSettlement = null; + } + } } } finally { this.#events.rejectActiveTurns(new DriverTurnCancelledError(reason)); diff --git a/src/runtimes/openai/app-server-event-bridge.ts b/src/runtimes/openai/app-server-event-bridge.ts index 71cfbe1..d4c5738 100644 --- a/src/runtimes/openai/app-server-event-bridge.ts +++ b/src/runtimes/openai/app-server-event-bridge.ts @@ -12,11 +12,12 @@ import { type OpenAiTerminalOutcome, } from "./app-server-event-state"; import { createRuntimeSourceEventId, toRuntimePublicId } from "../runtime-public-id"; +import { DriverCompletedTerminalSupersededError } from "../driver-event-publisher"; import { chunkOpenAiText, OpenAiAppServerItemEventBridge } from "./app-server-item-events"; import { isRecord, readArray, readNonEmptyString, readRecord, readString } from "./app-server-json"; import type { JsonObject } from "./app-server-json"; -import { OpenAiTurnTracker } from "./app-server-turn-tracker"; -import type { ServerNotificationMethod, ServerNotificationParams } from "./app-server-protocol"; +import { OpenAiTurnTracker, type OpenAiTurnAdmission } from "./app-server-turn-tracker"; +import type { ServerNotificationMethod } from "./app-server-protocol"; interface OpenAiAppServerEventBridgeOptions { beforeInterruptedTurn?(context: AgentDriverContext, turnId: string): Promise; @@ -31,6 +32,7 @@ interface OpenAiAppServerEventBridgeOptions { reason: string, closures: readonly DriverEventInput[], terminal: DriverEventInput, + cancellationSignal?: AbortSignal, ): Promise; requireThreadId(): string; } @@ -91,22 +93,36 @@ function toBoundedOpenAiTelemetry(payload: JsonObject, fields: readonly string[] return summary; } -function toOpenAiWarningEvents( - method: "guardianWarning" | "warning", +function toOpenAiUserFacingEvents( + method: + | "guardianWarning" + | "modelProvider/authRecoveryCompleted" + | "modelProvider/authRecoveryStarted" + | "warning", message: string, + details: JsonObject = {}, ): DriverEventInput[] { const chunks = chunkOpenAiText(message); + const warning = method === "guardianWarning" || method === "warning"; return chunks.map((content, index) => ({ - delivery: "lossless", + delivery: warning ? "lossless" : "best_effort", kind: "message.added", payload: { + ...details, ...(chunks.length === 1 ? {} : { chunkCount: chunks.length, chunkIndex: index }), content, - level: "warning", + level: warning ? "warning" : "info", messageId: createDriverId(), role: "agent", - subtype: method === "guardianWarning" ? "guardian_warning" : "warning", + subtype: + method === "guardianWarning" + ? "guardian_warning" + : method === "warning" + ? "warning" + : method === "modelProvider/authRecoveryStarted" + ? "model_provider_auth_recovery_started" + : "model_provider_auth_recovery_completed", }, })); } @@ -192,6 +208,47 @@ export class OpenAiAppServerEventBridge { return this.#turns.activeTurnIds(); } + beginTurnAdmission(runId: RunId, signal?: AbortSignal): OpenAiTurnAdmission { + return this.#turns.admitRootTurn(runId, signal); + } + + bindTurnAdmission(admission: OpenAiTurnAdmission, turnId: string): void { + this.#turns.bindRootTurn(admission, turnId); + } + + armTurnAdmission(admission: OpenAiTurnAdmission): void { + this.#turns.armRootTurn(admission); + } + + claimTurnAdmission( + admission: OpenAiTurnAdmission, + turnId: string, + runId: RunId, + signal?: AbortSignal, + ): Promise { + return this.#turns.claimRootTurn(admission, turnId, runId, signal); + } + + releaseTurnAdmission(admission: OpenAiTurnAdmission): void { + this.#turns.releaseRootTurn(admission); + } + + releaseTurnAdmissionSelection(admission: OpenAiTurnAdmission): void { + this.#turns.releaseRootTurnSelection(admission); + } + + admittedTurnId(admission: OpenAiTurnAdmission): string | null { + return this.#turns.admittedTurnId(admission); + } + + hasTerminalTurn(turnId: string): boolean { + return this.#turns.hasTerminal(turnId); + } + + hasAdmittedTerminalTurn(): boolean { + return this.#turns.hasAdmittedTerminalTurn(); + } + mapToolCallId(toolCallId: string): string { return this.#items.publicId(toolCallId); } @@ -224,7 +281,11 @@ export class OpenAiAppServerEventBridge { return; } const publicTurnId = this.publicTurnId(turnId); - const [taskClosure, ...itemClosures] = this.#itemEvents.terminalEvents({ kind: "cancelled" }); + const completionClosuresCommitted = this.#turns.completionClosuresCommitted(turnId); + const cancelledClosures = completionClosuresCommitted + ? [] + : this.#itemEvents.terminalEvents({ kind: "cancelled" }); + const [taskClosure, ...itemClosures] = cancelledClosures; try { await this.#options.pushTerminal( @@ -233,7 +294,7 @@ export class OpenAiAppServerEventBridge { turnClosureEvents({ eventName: "turn.cancelled", events: [ - taskClosure, + ...(taskClosure === undefined ? [] : [taskClosure]), { ...turnEventFields({ eventName: "turn.cancel.requested", publicTurnId, runId }), kind: "run.cancel.requested", @@ -332,20 +393,20 @@ export class OpenAiAppServerEventBridge { return this.#itemEvents.terminalEvents(outcome); } - async trackTurn(turnId: string, runId: RunId): Promise { - return this.#turns.track(turnId, runId); + async trackTurn(turnId: string, runId: RunId, signal?: AbortSignal): Promise { + return this.#turns.track(turnId, runId, signal); } - async handleNotification( + async handleNotification( context: AgentDriverContext, - method: M, - params: ServerNotificationParams[M], + method: ServerNotificationMethod, + params: JsonObject, ): Promise { const payload = isRecord(params) ? params : {}; const turnId = readNonEmptyString(payload, "turnId") ?? readNonEmptyString(readRecord(payload, "turn"), "id"); - const notificationMethod: ServerNotificationMethod = method; + const notificationMethod = method; // Native Codex subagents own child threads and turns. Their direct message // and lifecycle frames are provider-internal activity, not authoritative @@ -358,7 +419,6 @@ export class OpenAiAppServerEventBridge { ) { return; } - const item = readRecord(payload, "item"); const postTerminalSubAgentActivity = notificationMethod === "item/completed" && @@ -371,6 +431,12 @@ export class OpenAiAppServerEventBridge { } return; } + if ( + turnId !== null && + (!(await this.#turns.awaitRootTurnAdmission(turnId)) || !this.#turns.acceptsRootTurn(turnId)) + ) { + return; + } switch (notificationMethod) { case "configWarning": { @@ -382,6 +448,22 @@ export class OpenAiAppServerEventBridge { await this.#onWarning(context, payload, notificationMethod); return; } + case "modelProvider/authRecoveryStarted": + case "modelProvider/authRecoveryCompleted": { + const message = readString(payload, "message"); + if (message !== null) { + await this.#push( + context, + "driver.openai.model_provider.auth_recovery", + toOpenAiUserFacingEvents( + notificationMethod, + message, + toBoundedOpenAiTelemetry(payload, ["provider"]), + ), + ); + } + return; + } case "remoteControl/status/changed": { this.#onRemoteControl(context, payload); return; @@ -640,7 +722,7 @@ export class OpenAiAppServerEventBridge { if (identityRunId === null) { return; } - if (this.#turns.hasTurnStarted(input.turnId)) { + if (this.#turns.hasTurnStarted(input.turnId) || this.#turns.hasTerminal(input.turnId)) { return; } @@ -918,7 +1000,7 @@ export class OpenAiAppServerEventBridge { threadIdPresent: readString(params, "threadId") !== null, }); - await this.#push(context, `driver.openai.${method}`, toOpenAiWarningEvents(method, message)); + await this.#push(context, `driver.openai.${method}`, toOpenAiUserFacingEvents(method, message)); } async #onWorldWritableWarning(context: AgentDriverContext, params: JsonObject): Promise { @@ -1004,7 +1086,13 @@ export class OpenAiAppServerEventBridge { throw new Error("OpenAi turn/completed requires a terminal turn status."); } - const runId = this.#turns.activeRunId(turnId) ?? undefined; + const pendingTurn = this.#turns.pendingTurnContext(turnId); + if (pendingTurn !== null && !this.#turns.hasTurnStarted(turnId)) { + throw new Error("OpenAi turn/completed arrived before its turn/started notification."); + } + const runId = this.#turns.activeRunId(turnId) ?? pendingTurn?.runId; + const cancellationSignal = + this.#turns.cancellationSignal(turnId) ?? pendingTurn?.cancellationSignal ?? null; await this.publishRunStarted(context, { runId, turnId }); await this.#itemEvents.onTurnItems(context, params, turnId); @@ -1118,10 +1206,23 @@ export class OpenAiAppServerEventBridge { stopReason: "end_turn", }, }, + cancellationSignal ?? undefined, ); this.#finishSettlement(turnId, { kind: "completed" }); } catch (pushError) { this.#turns.cancelSettlement(turnId); + if ( + status === "completed" && + cancellationSignal?.aborted === true && + (pushError === cancellationSignal.reason || + (pushError instanceof DriverCompletedTerminalSupersededError && + pushError.cause === cancellationSignal.reason)) + ) { + if (pushError instanceof DriverCompletedTerminalSupersededError) { + this.#turns.markCompletionClosuresCommitted(turnId); + } + return; + } throw pushError; } } @@ -1165,7 +1266,7 @@ export class OpenAiAppServerEventBridge { return; } - const runId = this.#turns.activeRunId(turnId) ?? undefined; + const runId = this.#turns.activeRunId(turnId) ?? this.#turns.pendingTurnContext(turnId)?.runId; await this.publishRunStarted(context, { runId, turnId }); } diff --git a/src/runtimes/openai/app-server-protocol-types.ts b/src/runtimes/openai/app-server-protocol-types.ts index 73f1d26..bd1a5b7 100644 --- a/src/runtimes/openai/app-server-protocol-types.ts +++ b/src/runtimes/openai/app-server-protocol-types.ts @@ -51,10 +51,6 @@ export interface ClientRequestParams { "turn/start": TurnStartParams; } -export type ServerNotificationParams = { - [Method in ServerNotificationMethod]: JsonObject; -}; - export interface ParsedServerNotification { emittedAtMs?: number; method: ServerNotificationMethod; diff --git a/src/runtimes/openai/app-server-turn-tracker.ts b/src/runtimes/openai/app-server-turn-tracker.ts index 868ba98..7d23f6d 100644 --- a/src/runtimes/openai/app-server-turn-tracker.ts +++ b/src/runtimes/openai/app-server-turn-tracker.ts @@ -4,6 +4,8 @@ import type { DriverTurnCancelledError } from "../../core/driver-runtime-state"; import type { RunId } from "../../protocol/id"; interface ActiveOpenAiTurn { + cancellationSignal: AbortSignal | null; + completionClosuresCommitted: boolean; nativeTurnId: string; promise: Promise; reject(error: Error): void; @@ -11,10 +13,28 @@ interface ActiveOpenAiTurn { runId: RunId; } -type TerminalOpenAiTurn = +type OpenAiTurnTerminalOutcome = | { kind: "completed" } | { error: DriverTurnCancelledError | Error; kind: "failed" }; +type TerminalOpenAiTurn = OpenAiTurnTerminalOutcome & { runId: RunId | null }; + +export interface OpenAiTurnAdmission { + readonly token: symbol; +} + +interface PendingOpenAiTurn { + readonly admission: OpenAiTurnAdmission; + armed: boolean; + readonly boundTurnId: Promise; + readonly cancellationSignal: AbortSignal | null; + completionClosuresCommitted: boolean; + nativeTurnId: string | null; + readonly resolveBoundTurnId: (turnId: string | null) => void; + readonly runId: RunId; + selectionReleased: boolean; +} + const MAX_RETAINED_TURNS = 1_024; const MAX_RETAINED_NATIVE_TURN_ID_BYTES = 256; @@ -40,6 +60,9 @@ function rememberBounded(map: Map, turnId: string, value: T): void export class OpenAiTurnTracker { readonly #activeTurns = new Map(); + readonly #ignoredTurnIds = new Map(); + #pendingRootTurn: PendingOpenAiTurn | null = null; + #rootAdmissionsEnforced = false; readonly #settlingTurnIds = new Set(); readonly #terminalTurns = new Map(); readonly #startedTurnIds = new Map(); @@ -52,12 +75,193 @@ export class OpenAiTurnTracker { return this.#activeTurns.get(retainedOpenAiTurnKey(turnId))?.runId ?? null; } + admitRootTurn(runId: RunId, signal?: AbortSignal): OpenAiTurnAdmission { + if (this.#pendingRootTurn !== null) { + throw new Error("OpenAI root turn admission is already pending."); + } + + this.#rootAdmissionsEnforced = true; + const admission = { token: Symbol("openai-root-turn") }; + const boundTurnId = Promise.withResolvers(); + this.#pendingRootTurn = { + admission, + armed: false, + boundTurnId: boundTurnId.promise, + cancellationSignal: signal ?? null, + completionClosuresCommitted: false, + nativeTurnId: null, + resolveBoundTurnId: boundTurnId.resolve, + runId, + selectionReleased: false, + }; + return admission; + } + + armRootTurn(admission: OpenAiTurnAdmission): void { + const pending = this.#pendingRootTurn; + if (pending?.admission !== admission || pending.selectionReleased) { + throw new Error("OpenAI root turn admission is no longer active."); + } + pending.armed = true; + } + + bindRootTurn(admission: OpenAiTurnAdmission, turnId: string): void { + const pending = this.#pendingRootTurn; + if (pending?.admission !== admission) { + throw new Error("OpenAI root turn admission is no longer active."); + } + if (!pending.armed || pending.selectionReleased) { + throw new Error("OpenAI root turn admission is not awaiting a response."); + } + if (pending.nativeTurnId !== null && pending.nativeTurnId !== turnId) { + throw new Error("OpenAI root turn admission received a different native turn."); + } + if (pending.nativeTurnId === turnId) { + return; + } + + pending.nativeTurnId = turnId; + this.#ignoredTurnIds.delete(retainedOpenAiTurnKey(turnId)); + pending.resolveBoundTurnId(turnId); + } + + async awaitRootTurnAdmission(turnId: string): Promise { + const pending = this.#pendingRootTurn; + if (pending === null) { + return !this.#ignoredTurnIds.has(retainedOpenAiTurnKey(turnId)); + } + if (!pending.armed) { + rememberBounded(this.#ignoredTurnIds, turnId, true); + return false; + } + + const boundTurnId = await pending.boundTurnId; + if (boundTurnId === null) { + return false; + } + if (boundTurnId === turnId) { + return true; + } + rememberBounded(this.#ignoredTurnIds, turnId, true); + return false; + } + + pendingTurnContext( + turnId: string, + ): { cancellationSignal: AbortSignal | null; runId: RunId } | null { + const pending = this.#pendingRootTurn; + return pending?.nativeTurnId === turnId + ? { cancellationSignal: pending.cancellationSignal, runId: pending.runId } + : null; + } + + hasPendingRootTurn(): boolean { + return this.#pendingRootTurn !== null; + } + + admittedTurnId(admission: OpenAiTurnAdmission): string | null { + const pending = this.#pendingRootTurn; + return pending?.admission === admission ? pending.nativeTurnId : null; + } + + hasAdmittedTerminalTurn(): boolean { + const turnId = this.#pendingRootTurn?.nativeTurnId; + return turnId !== null && turnId !== undefined && this.hasTerminal(turnId); + } + + acceptsRootTurn(turnId: string): boolean { + if (!this.#rootAdmissionsEnforced) { + return true; + } + + const key = retainedOpenAiTurnKey(turnId); + return ( + this.#pendingRootTurn?.nativeTurnId === turnId || + this.#activeTurns.has(key) || + this.#settlingTurnIds.has(key) || + this.#terminalTurns.has(key) + ); + } + + claimRootTurn( + admission: OpenAiTurnAdmission, + turnId: string, + runId: RunId, + signal?: AbortSignal, + ): Promise { + const pending = this.#pendingRootTurn; + if (pending?.admission !== admission) { + throw new Error("OpenAI root turn admission is no longer active."); + } + if (pending.runId !== runId || pending.cancellationSignal !== (signal ?? null)) { + throw new Error("OpenAI root turn admission context changed before its response."); + } + if (pending.nativeTurnId !== turnId) { + throw new Error("OpenAI turn/start response was not bound to its admission."); + } + + this.#pendingRootTurn = null; + const tracked = this.#track(turnId, runId, signal); + const activeTurn = this.#activeTurns.get(retainedOpenAiTurnKey(turnId)); + if (activeTurn !== undefined) { + activeTurn.completionClosuresCommitted = pending.completionClosuresCommitted; + } + return tracked; + } + + releaseRootTurn(admission: OpenAiTurnAdmission): void { + const pending = this.#pendingRootTurn; + if (pending?.admission === admission) { + pending.resolveBoundTurnId(null); + this.#pendingRootTurn = null; + } + } + + releaseRootTurnSelection(admission: OpenAiTurnAdmission): void { + const pending = this.#pendingRootTurn; + if ( + pending?.admission === admission && + pending.nativeTurnId === null && + !pending.selectionReleased + ) { + pending.selectionReleased = true; + pending.resolveBoundTurnId(null); + } + } + + cancellationSignal(turnId: string): AbortSignal | null { + return this.#activeTurns.get(retainedOpenAiTurnKey(turnId))?.cancellationSignal ?? null; + } + + completionClosuresCommitted(turnId: string): boolean { + return ( + this.#activeTurns.get(retainedOpenAiTurnKey(turnId))?.completionClosuresCommitted ?? + (this.#pendingRootTurn?.nativeTurnId === turnId + ? this.#pendingRootTurn.completionClosuresCommitted + : false) + ); + } + + markCompletionClosuresCommitted(turnId: string): void { + const turn = this.#activeTurns.get(retainedOpenAiTurnKey(turnId)); + if (turn !== undefined) { + turn.completionClosuresCommitted = true; + return; + } + if (this.#pendingRootTurn?.nativeTurnId === turnId) { + this.#pendingRootTurn.completionClosuresCommitted = true; + } + } + activeTurnIds(): string[] { return [...this.#activeTurns.values()].map(({ nativeTurnId }) => nativeTurnId); } clearActiveTurns(): void { + this.#pendingRootTurn?.resolveBoundTurnId(null); + this.#pendingRootTurn = null; this.#activeTurns.clear(); + this.#ignoredTurnIds.clear(); this.#settlingTurnIds.clear(); this.#startedTurnIds.clear(); this.#terminalTurns.clear(); @@ -105,7 +309,7 @@ export class OpenAiTurnTracker { this.#settlingTurnIds.delete(retainedOpenAiTurnKey(turnId)); } - finishSettlement(turnId: string, terminalTurn: TerminalOpenAiTurn): boolean { + finishSettlement(turnId: string, terminalTurn: OpenAiTurnTerminalOutcome): boolean { const key = retainedOpenAiTurnKey(turnId); if (!this.#settlingTurnIds.delete(key) || this.#terminalTurns.has(key)) { return false; @@ -115,7 +319,7 @@ export class OpenAiTurnTracker { return true; } - settle(turnId: string, terminalTurn: TerminalOpenAiTurn): boolean { + settle(turnId: string, terminalTurn: OpenAiTurnTerminalOutcome): boolean { if (this.hasTerminal(turnId)) { return false; } @@ -124,11 +328,14 @@ export class OpenAiTurnTracker { return true; } - #recordTerminal(turnId: string, terminalTurn: TerminalOpenAiTurn): void { + #recordTerminal(turnId: string, terminalTurn: OpenAiTurnTerminalOutcome): void { const key = retainedOpenAiTurnKey(turnId); this.#startedTurnIds.delete(key); - rememberBounded(this.#terminalTurns, turnId, terminalTurn); const activeTurn = this.#activeTurns.get(key); + rememberBounded(this.#terminalTurns, turnId, { + ...terminalTurn, + runId: activeTurn?.runId ?? this.pendingTurnContext(turnId)?.runId ?? null, + }); if (activeTurn === undefined) { return; @@ -143,16 +350,30 @@ export class OpenAiTurnTracker { this.#activeTurns.delete(key); } - async track(turnId: string, runId: RunId): Promise { + track(turnId: string, runId: RunId, signal?: AbortSignal): Promise { + try { + return this.#track(turnId, runId, signal); + } catch (error) { + return Promise.reject(error); + } + } + + #track(turnId: string, runId: RunId, signal?: AbortSignal): Promise { const key = retainedOpenAiTurnKey(turnId); const terminalTurn = this.#terminalTurns.get(key); if (terminalTurn?.kind === "completed") { - return; + if (terminalTurn.runId !== null && terminalTurn.runId !== runId) { + throw new Error("OpenAI terminal turn belongs to another run."); + } + return Promise.resolve(); } if (terminalTurn?.kind === "failed") { - throw terminalTurn.error; + if (terminalTurn.runId !== null && terminalTurn.runId !== runId) { + throw new Error("OpenAI terminal turn belongs to another run."); + } + return Promise.reject(terminalTurn.error); } const activeTurn = this.#activeTurns.get(key); @@ -161,12 +382,17 @@ export class OpenAiTurnTracker { if (activeTurn.runId !== runId) { throw new Error("OpenAI turn is already tracked by another run."); } + if (activeTurn.cancellationSignal !== (signal ?? null)) { + throw new Error("OpenAI turn cancellation signal changed while it was active."); + } return activeTurn.promise; } const turn = Promise.withResolvers(); this.#activeTurns.set(key, { + cancellationSignal: signal ?? null, + completionClosuresCommitted: false, nativeTurnId: turnId, promise: turn.promise, reject: turn.reject, diff --git a/src/runtimes/openai/generated-json-schema/README.md b/src/runtimes/openai/generated-json-schema/README.md index d990c80..f7a3d01 100644 --- a/src/runtimes/openai/generated-json-schema/README.md +++ b/src/runtimes/openai/generated-json-schema/README.md @@ -1,6 +1,6 @@ # OpenAI app-server JSON schemas -These eight runtime schemas are selected from `@openai/codex@0.151.0` output and must not be edited by hand. +These eight runtime schemas are selected from `@openai/codex@0.152.0` output and must not be edited by hand. ```sh bun scripts/sync-openai-generated.mjs diff --git a/src/runtimes/openai/generated-json-schema/ServerNotification.json b/src/runtimes/openai/generated-json-schema/ServerNotification.json index 4c6bdee..b4ad59e 100644 --- a/src/runtimes/openai/generated-json-schema/ServerNotification.json +++ b/src/runtimes/openai/generated-json-schema/ServerNotification.json @@ -430,6 +430,24 @@ } ] }, + "AuthRecoveryNotification": { + "properties": { + "message": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": ["message", "provider", "threadId", "turnId"], + "type": "object" + }, "AutoReviewDecisionSource": { "description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.", "enum": ["agent"], @@ -6046,6 +6064,36 @@ "title": "Model/verificationNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": ["modelProvider/authRecoveryStarted"], + "title": "ModelProvider/authRecoveryStartedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AuthRecoveryNotification" + } + }, + "required": ["method", "params"], + "title": "ModelProvider/authRecoveryStartedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": ["modelProvider/authRecoveryCompleted"], + "title": "ModelProvider/authRecoveryCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AuthRecoveryNotification" + } + }, + "required": ["method", "params"], + "title": "ModelProvider/authRecoveryCompletedNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/src/runtimes/openai/generated-json-schema/ServerRequest.json b/src/runtimes/openai/generated-json-schema/ServerRequest.json index 3ac9133..0bf1d21 100644 --- a/src/runtimes/openai/generated-json-schema/ServerRequest.json +++ b/src/runtimes/openai/generated-json-schema/ServerRequest.json @@ -1093,6 +1093,21 @@ "required": ["message", "mode", "requestedSchema"], "type": "object" }, + { + "properties": { + "_meta": true, + "message": { + "type": "string" + }, + "mode": { + "enum": ["openaiForm"], + "type": "string" + }, + "requestedSchema": true + }, + "required": ["message", "mode", "requestedSchema"], + "type": "object" + }, { "properties": { "_meta": true, diff --git a/src/runtimes/openai/generated/ProtocolMethods.ts b/src/runtimes/openai/generated/ProtocolMethods.ts index 050737b..e38094e 100644 --- a/src/runtimes/openai/generated/ProtocolMethods.ts +++ b/src/runtimes/openai/generated/ProtocolMethods.ts @@ -59,6 +59,8 @@ export type ServerNotificationMethod = | "thread/compacted" | "model/rerouted" | "model/verification" + | "modelProvider/authRecoveryStarted" + | "modelProvider/authRecoveryCompleted" | "turn/moderationMetadata" | "model/safetyBuffering/updated" | "warning" diff --git a/src/runtimes/openai/generated/README.md b/src/runtimes/openai/generated/README.md index d70bb37..5a6bab2 100644 --- a/src/runtimes/openai/generated/README.md +++ b/src/runtimes/openai/generated/README.md @@ -2,7 +2,7 @@ This directory contains the transitive closure of the OpenAI app-server types imported by the driver. -They correspond to OpenAI app-server runtime version `0.151.0`, which is also pinned by the CLI dependency and container image. +They correspond to OpenAI app-server runtime version `0.152.0`, which is also pinned by the CLI dependency and container image. Runtime validation and the complete supported server method sets come from the adjacent JSON Schemas. diff --git a/tests/acp-driver-backend.test.ts b/tests/acp-driver-backend.test.ts index 7782b84..68216f3 100644 --- a/tests/acp-driver-backend.test.ts +++ b/tests/acp-driver-backend.test.ts @@ -9,7 +9,9 @@ import { DriverPermissionBroker, PermissionEventDeliveryError, } from "../src/core/driver-permission-broker"; +import { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; import type { DriverRuntimeEventPort } from "../src/core/driver-runtime-io"; +import { DriverTerminalStateMachine } from "../src/core/driver-terminal-state"; import { createDisabledLogger } from "../src/observability"; import type { AgentDriverFilePort, AgentDriverPermissionPort } from "../src/host-ports"; import type { DriverBootPayload } from "../src/protocol/boot"; @@ -24,6 +26,7 @@ import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { settlePromiseWithTimeout } from "../src/utils/async"; import { waitForAcpTestCondition } from "./acp-test-helpers"; import { driverBootPayload, DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; +import { createDispatcher, FakeDriverRuntimeIo } from "./driver-runtime-boundary-fixtures"; const FAKE_AGENT = String.raw` const { appendFileSync, existsSync } = require("node:fs"); @@ -309,6 +312,10 @@ const handle = (message) => { } return; } + if (message.params.prompt[0]?.text === "provider-cancel") { + result = { stopReason: "cancelled" }; + break; + } if (message.params.prompt[0]?.text === "hang") { pendingPromptId = message.id; send({ @@ -1558,9 +1565,15 @@ describe("ACP driver backend lifecycle", () => { "ACP session/prompt request", ); - await expect( - harness.backend.cancelActiveTurn(harness.context, "test cancellation"), - ).resolves.toBeUndefined(); + const gate = harness.blockNext("run.cancel.requested"); + const cancellation = harness.backend.cancelActiveTurn(harness.context, "test cancellation"); + await gate.entered; + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/cancel"), + "ACP session/cancel request", + ); + gate.release(); + await expect(cancellation).resolves.toBeUndefined(); await waitForAcpTestCondition( async () => (await harness.methods()).includes("session/resume"), "ACP session/resume request", @@ -1835,6 +1848,96 @@ describe("ACP driver backend lifecycle", () => { } }); + test("cancels an unresponsive provider only after the request event is durable", async () => { + const harness = await createHarness(); + const state = new DriverTerminalStateMachine(); + const ticket = state.beginRun(DRIVER_TEST_IDS.runId as RunId); + const promptRequested = Promise.withResolvers(); + const cancellationPushEntered = Promise.withResolvers(); + const releaseCancellationPush = Promise.withResolvers(); + const barrierEntered = Promise.withResolvers(); + let resumeAllowedAtBarrier: boolean | null = null; + const events: DriverEventInput[] = []; + const turn = new AcpTurnController( + async (_context, _reason, pushed) => { + if (pushed.some(({ kind }) => kind === "run.cancel.requested")) { + cancellationPushEntered.resolve(); + await releaseCancellationPush.promise; + } + events.push(...pushed); + }, + async (_context, _providerPromptAdmitted, resumeSignal) => { + resumeAllowedAtBarrier = !resumeSignal.aborted; + barrierEntered.resolve(); + }, + ); + const clientRequests = new AcpClientRequestHandler({ + allowedRoots: [process.cwd()], + cwd: process.cwd(), + env: {}, + isCancelling: () => turn.isCancelling(), + nativeSessionId: () => "native-session-1", + onUpdateFailure: () => {}, + push: async () => {}, + turnEvents: turn.events, + }); + const connection = { + notify: () => new Promise(() => {}), + request: async () => { + promptRequested.resolve(); + return new Promise(() => {}); + }, + } as unknown as ClientContext; + + try { + const input = turn.handleInput( + harness.context, + { text: "cancel me" }, + DRIVER_TEST_IDS.runId as RunId, + connection, + "native-session-1", + clientRequests, + ticket.signal, + ); + void input.catch(() => {}); + await promptRequested.promise; + expect(state.claimCancellation(ticket, "test cancellation", "turn.cancel")).toBe("claimed"); + const cancel = turn.cancel( + harness.context, + "test cancellation", + connection, + "native-session-1", + ); + + await cancellationPushEntered.promise; + await expect(cancel).resolves.toBeUndefined(); + expect( + await settlePromiseWithTimeout(barrierEntered.promise, { + label: "ACP cancellation request acknowledgement", + timeoutMs: 25, + }), + ).toMatchObject({ status: "timed_out" }); + expect(state.claimCancellation(ticket, "test shutdown", "shutdown")).toBe("already_claimed"); + releaseCancellationPush.resolve(); + await expect(input).rejects.toThrow("cancelled"); + await barrierEntered.promise; + expect(resumeAllowedAtBarrier).toBe(false); + + const eventKinds = events.map(({ kind }) => kind); + expect(eventKinds.indexOf("run.cancel.requested")).toBeLessThan( + eventKinds.indexOf("run.cancelled"), + ); + expect( + eventKinds.filter( + (kind) => kind === "run.cancelled" || kind === "run.completed" || kind === "run.failed", + ), + ).toEqual(["run.cancelled"]); + } finally { + releaseCancellationPush.resolve(); + await harness.destroy(); + } + }); + test("does not hold cancellation acknowledgement behind provider recycle", async () => { const harness = await createHarness(); const promptRequested = Promise.withResolvers(); @@ -2251,7 +2354,15 @@ describe("ACP driver backend lifecycle", () => { releasePermission.resolve(); await expect(input).rejects.toThrow("cancelled"); - expect(harness.events).toContainEqual(expect.objectContaining({ kind: "run.cancelled" })); + expect(harness.events).toContainEqual( + expect.objectContaining({ + kind: "run.cancelled", + payload: expect.objectContaining({ requestedBy: "provider" }), + }), + ); + expect(harness.events).not.toContainEqual( + expect.objectContaining({ kind: "run.cancel.requested" }), + ); expect(harness.events).not.toContainEqual(expect.objectContaining({ kind: "run.failed" })); } finally { releasePermission.resolve(); @@ -2287,6 +2398,361 @@ describe("ACP driver backend lifecycle", () => { } }); + test("lets a cancellation request use the durable event delivery budget", async () => { + const harness = await createHarness(); + const gateEntered = Promise.withResolvers(); + const releaseGate = Promise.withResolvers(); + let gateOpen = false; + const socket = new FakeDriverRuntimeIo([ + { + commandId: "acp-durable-cancel-input", + input: { text: "hang" }, + kind: "input.start", + requestId: "acp-durable-cancel-request", + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: "acp-durable-cancel-command", + kind: "turn.cancel", + reason: "test.cancel", + runId: DRIVER_TEST_IDS.runId, + }, + ]); + const pushEvents = socket.pushEvents.bind(socket); + socket.pushEvents = async (input) => { + if (!gateOpen && input.events.some(({ kind }) => kind === "run.cancel.requested")) { + gateOpen = true; + gateEntered.resolve(); + await releaseGate.promise; + } + return pushEvents(input); + }; + const runtimeState = new DriverRuntimeStateMachine("ready"); + const { dispatcher, logger, shutdownCalls } = createDispatcher({ + backend: harness.backend, + isShuttingDown: () => socket.isDrained() && socket.currentRunId() === null, + runtimeState, + }); + + try { + const running = dispatcher.run(socket, logger); + await gateEntered.promise; + await Bun.sleep(2_100); + releaseGate.resolve(); + await running; + + const events = socket.pushedEvents.flatMap(({ events }) => events); + expect( + events + .filter(({ kind }) => ["run.cancelled", "run.completed", "run.failed"].includes(kind)) + .map(({ kind }) => kind), + ).toEqual(["run.cancelled"]); + expect(events.filter(({ kind }) => kind === "run.cancel.requested")).toHaveLength(1); + expect(runtimeState.status()).toBe("ready"); + expect(shutdownCalls).toEqual([]); + } finally { + releaseGate.resolve(); + await harness.destroy(); + } + }, 7_000); + + test("honors dispatcher cancellation before the completed terminal is selected", async () => { + const harness = await createHarness({ blockResume: true }); + const windowEntered = Promise.withResolvers(); + const releaseWindow = Promise.withResolvers(); + const cancellationClaimed = Promise.withResolvers(); + const socket = new FakeDriverRuntimeIo([ + { + commandId: "acp-terminal-race-input", + input: { text: "complete" }, + kind: "input.start", + requestId: "acp-terminal-race-request", + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: "acp-terminal-race-cancel", + kind: "turn.cancel", + reason: "test.cancel", + runId: DRIVER_TEST_IDS.runId, + }, + ]); + const nextCommand = socket.nextCommand.bind(socket); + socket.nextCommand = async (signal) => { + const command = await nextCommand(signal); + if (command?.kind === "turn.cancel") { + await windowEntered.promise; + } + return command; + }; + const registerRunTerminalBarrier = socket.registerRunTerminalBarrier.bind(socket); + socket.registerRunTerminalBarrier = (barrier) => + registerRunTerminalBarrier((events) => { + const pending = barrier(events); + if (!events.some(({ kind }) => kind === "run.completed")) { + return pending; + } + return Promise.resolve(pending).then(async () => { + windowEntered.resolve(); + await releaseWindow.promise; + }); + }); + const claimRunCancellation = socket.claimRunCancellation.bind(socket); + socket.claimRunCancellation = (ticket, reason) => { + const result = claimRunCancellation(ticket, reason); + cancellationClaimed.resolve(); + return result; + }; + const runtimeState = new DriverRuntimeStateMachine("ready"); + const { dispatcher, logger, shutdownCalls } = createDispatcher({ + backend: harness.backend, + isShuttingDown: () => socket.isDrained() && socket.currentRunId() === null, + runtimeState, + }); + + try { + const running = dispatcher.run(socket, logger); + await windowEntered.promise; + await cancellationClaimed.promise; + releaseWindow.resolve(); + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/resume"), + "ACP session/resume request", + ); + await Bun.sleep(2_100); + await writeFile(harness.resumeGatePath, "resume"); + await running; + + const events = socket.pushedEvents.flatMap(({ events }) => events); + expect( + events + .filter( + ({ kind }) => + kind === "run.cancelled" || kind === "run.completed" || kind === "run.failed", + ) + .map(({ kind }) => kind), + ).toEqual(["run.cancelled"]); + expect(events.filter(({ kind }) => kind === "run.cancel.requested")).toHaveLength(1); + expect( + events + .filter(({ kind }) => kind === "message.completed" || kind === "message.cancelled") + .map(({ kind }) => kind), + ).toEqual(["message.completed"]); + expect(socket.updates).toEqual([ + { commandId: "acp-terminal-race-input", status: "accepted" }, + { commandId: "acp-terminal-race-cancel", status: "accepted" }, + { commandId: "acp-terminal-race-input", status: "cancelled" }, + { commandId: "acp-terminal-race-cancel", status: "completed" }, + ]); + expect(socket.failedRuns).toEqual([]); + expect(shutdownCalls).toEqual([]); + expect(runtimeState.status()).toBe("ready"); + } finally { + releaseWindow.resolve(); + await writeFile(harness.resumeGatePath, "cleanup").catch(() => {}); + await harness.destroy(); + } + }, 7_000); + + test("interrupts a provider-cancelled resume when session stop arrives", async () => { + const harness = await createHarness({ blockResume: true }); + const runtimeState = new DriverRuntimeStateMachine("ready"); + const socket = new FakeDriverRuntimeIo([ + { + commandId: "acp-provider-cancel-input", + input: { text: "provider-cancel" }, + kind: "input.start", + requestId: "acp-provider-cancel-request", + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: "acp-provider-cancel-stop", + kind: "session.stop", + reason: "test.stop", + }, + ]); + const nextCommand = socket.nextCommand.bind(socket); + socket.nextCommand = async (signal) => { + const command = await nextCommand(signal); + if (command?.kind === "session.stop") { + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/resume"), + "blocked ACP session/resume request", + ); + } + return command; + }; + const { dispatcher, logger } = createDispatcher({ + backend: harness.backend, + isShuttingDown: () => runtimeState.isShuttingDown(), + runtimeState, + shutdown: async (_runtimeIo, reason) => + harness.backend.stop(harness.context, reason, new AbortController().signal), + }); + const running = dispatcher.run(socket, logger); + + try { + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/resume"), + "blocked ACP session/resume request", + ); + expect( + await settlePromiseWithTimeout(running, { + label: "provider-cancelled ACP session stop", + timeoutMs: 1_500, + }), + ).toMatchObject({ status: "completed" }); + + const events = socket.pushedEvents.flatMap(({ events }) => events); + const eventKinds = events.map(({ kind }) => kind); + expect( + events + .filter(({ kind }) => ["run.cancelled", "run.completed", "run.failed"].includes(kind)) + .map(({ kind }) => kind), + ).toEqual(["run.cancelled"]); + expect(eventKinds.filter((kind) => kind === "run.cancel.requested")).toHaveLength(1); + expect(eventKinds.indexOf("run.cancel.requested")).toBeLessThan( + eventKinds.indexOf("run.cancelled"), + ); + expect(events.filter(({ kind }) => kind === "session.resumed")).toHaveLength(0); + expect(runtimeState.status()).toBe("stopped"); + } finally { + await writeFile(harness.resumeGatePath, "cleanup").catch(() => {}); + await running.catch(() => {}); + await harness.destroy(); + } + }, 7_000); + + test("interrupts a turn-cancel resume when shutdown upgrades the claim", async () => { + const harness = await createHarness({ blockResume: true }); + const shutdown = new AbortController(); + const runtimeState = new DriverRuntimeStateMachine("ready"); + const socket = new FakeDriverRuntimeIo([ + { + commandId: "acp-cancel-upgrade-input", + input: { text: "hang" }, + kind: "input.start", + requestId: "acp-cancel-upgrade-request", + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: "acp-cancel-upgrade-command", + kind: "turn.cancel", + reason: "test.cancel", + runId: DRIVER_TEST_IDS.runId, + }, + ]); + const nextCommand = socket.nextCommand.bind(socket); + socket.nextCommand = async (signal) => { + const command = await nextCommand(signal); + if (command?.kind === "turn.cancel") { + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/prompt"), + "ACP session/prompt request", + ); + } + return command; + }; + const { dispatcher, logger } = createDispatcher({ + backend: harness.backend, + isShuttingDown: () => shutdown.signal.aborted, + runtimeState, + shutdownSignal: shutdown.signal, + }); + const running = dispatcher.run(socket, logger); + + try { + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/resume"), + "blocked ACP session/resume after turn cancellation", + ); + shutdown.abort(new Error("test shutdown")); + expect( + await settlePromiseWithTimeout(running, { + label: "upgraded ACP turn cancellation", + timeoutMs: 1_500, + }), + ).toMatchObject({ status: "completed" }); + + const events = socket.pushedEvents.flatMap(({ events }) => events); + expect( + events + .filter(({ kind }) => ["run.cancelled", "run.completed", "run.failed"].includes(kind)) + .map(({ kind }) => kind), + ).toEqual(["run.cancelled"]); + expect(events.filter(({ kind }) => kind === "session.resumed")).toHaveLength(0); + } finally { + shutdown.abort(new Error("test cleanup")); + await writeFile(harness.resumeGatePath, "cleanup").catch(() => {}); + await running.catch(() => {}); + await harness.destroy(); + } + }, 7_000); + + test("stops an active session without resuming its cancelled provider", async () => { + const harness = await createHarness(); + const runtimeState = new DriverRuntimeStateMachine("ready"); + const socket = new FakeDriverRuntimeIo([ + { + commandId: "acp-stop-input", + input: { text: "hang" }, + kind: "input.start", + requestId: "acp-stop-request", + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: "acp-stop-session", + kind: "session.stop", + reason: "test.stop", + }, + ]); + const nextCommand = socket.nextCommand.bind(socket); + socket.nextCommand = async (signal) => { + const command = await nextCommand(signal); + if (command?.kind === "session.stop") { + await waitForAcpTestCondition( + async () => (await harness.methods()).includes("session/prompt"), + "ACP session/prompt request", + ); + } + return command; + }; + const { dispatcher, logger } = createDispatcher({ + backend: harness.backend, + isShuttingDown: () => runtimeState.isShuttingDown(), + runtimeState, + shutdown: async (_runtimeIo, reason) => + harness.backend.stop(harness.context, reason, new AbortController().signal), + }); + + try { + await dispatcher.run(socket, logger); + + const events = socket.pushedEvents.flatMap(({ events }) => events); + expect( + events + .filter( + ({ kind }) => + kind === "run.cancelled" || kind === "run.completed" || kind === "run.failed", + ) + .map(({ kind }) => kind), + ).toEqual(["run.cancelled"]); + expect(events.filter(({ kind }) => kind === "run.cancel.requested")).toHaveLength(1); + expect(await harness.methods()).not.toContain("session/resume"); + expect(socket.updates).toContainEqual({ + commandId: "acp-stop-input", + status: "cancelled", + }); + expect(socket.updates).toContainEqual({ + commandId: "acp-stop-session", + status: "completed", + }); + expect(socket.completedRunReasons).toEqual(["completed"]); + expect(runtimeState.status()).toBe("stopped"); + } finally { + await harness.destroy(); + } + }); + test("drains burst updates sent immediately before the prompt response", async () => { const harness = await createHarness(); diff --git a/tests/acp-provider-fixtures.test.ts b/tests/acp-provider-fixtures.test.ts index 07a57fa..8a1378a 100644 --- a/tests/acp-provider-fixtures.test.ts +++ b/tests/acp-provider-fixtures.test.ts @@ -15,13 +15,7 @@ interface AcpProviderFixtureCase { | { readonly stopReason: StopReason; readonly usage: unknown } | undefined; readonly expectedEvents: readonly unknown[]; - readonly failPrompt?: - | { - readonly code: string; - readonly message: string; - readonly recoverable?: boolean | undefined; - } - | undefined; + readonly failPrompt?: Parameters[0] | undefined; readonly permissionRequest?: { readonly params: unknown; readonly requestId: string } | undefined; readonly sessionReady?: | { @@ -62,15 +56,7 @@ function appAcpFixture(fixture: AcpProviderFixtureCase): DriverEventInput[] { } if (fixture.failPrompt !== undefined) { - events.push( - ...state.failPrompt({ - code: fixture.failPrompt.code, - message: fixture.failPrompt.message, - ...(fixture.failPrompt.recoverable === undefined - ? {} - : { recoverable: fixture.failPrompt.recoverable }), - }), - ); + events.push(...state.failPrompt(fixture.failPrompt)); } if (fixture.completePrompt !== undefined) { diff --git a/tests/agent-driver-kernel-commands.test.ts b/tests/agent-driver-kernel-commands.test.ts index f28d99c..10c7a80 100644 --- a/tests/agent-driver-kernel-commands.test.ts +++ b/tests/agent-driver-kernel-commands.test.ts @@ -1,12 +1,11 @@ import { describe, expect, test } from "bun:test"; -import { ACTIVE_TURN_CANCEL_GRACE_MS } from "../src/core/driver-command-dispatcher"; +import { ACTIVE_INPUT_SETTLE_GRACE_MS } from "../src/core/driver-command-dispatcher"; import type { AgentDriverContext, AgentDriverContextPortOverrides, } from "../src/core/agent-driver-backend"; import { AgentDriverKernelCore } from "../src/core/agent-driver-kernel"; -import { DRIVER_EVENT_DELIVERY_TIMEOUT_MS } from "../src/core/driver-runtime-io"; import type { DriverEventInput } from "../src/protocol/events"; import { RUNTIME_COMMAND_MAX_UTF8_BYTES, @@ -798,16 +797,21 @@ describe("AgentDriverKernelCore", () => { .catch(() => {}); await inputEntered.promise; const nativeSetTimeout = globalThis.setTimeout; + let activeInputTimeouts = 0; const acceleratedSetTimeout = ( callback: (...args: unknown[]) => void, timeout?: number, ...args: unknown[] - ) => - nativeSetTimeout( + ) => { + if (timeout === ACTIVE_INPUT_SETTLE_GRACE_MS) { + activeInputTimeouts += 1; + } + return nativeSetTimeout( callback, - timeout === ACTIVE_TURN_CANCEL_GRACE_MS + DRIVER_EVENT_DELIVERY_TIMEOUT_MS ? 10 : timeout, + timeout === ACTIVE_INPUT_SETTLE_GRACE_MS ? 10 : timeout, ...args, ); + }; globalThis.setTimeout = acceleratedSetTimeout as typeof setTimeout; try { @@ -817,6 +821,7 @@ describe("AgentDriverKernelCore", () => { expect(first[0]?.status).toBe("rejected"); expect(second[0]?.status).toBe("rejected"); expect(inputSettled).toBe(false); + expect(activeInputTimeouts).toBe(1); } finally { globalThis.setTimeout = nativeSetTimeout; releaseInput.resolve(); diff --git a/tests/architecture-boundaries.test.ts b/tests/architecture-boundaries.test.ts index 2818eb1..460c3c5 100644 --- a/tests/architecture-boundaries.test.ts +++ b/tests/architecture-boundaries.test.ts @@ -1,84 +1,55 @@ import { describe, expect, test } from "bun:test"; -import { dirname, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; const repositoryRoot = fileURLToPath(new URL("../", import.meta.url)); -const packageManifest = resolve(repositoryRoot, "package.json"); -const sourceRoot = resolve(repositoryRoot, "src"); -const transpiler = new Bun.Transpiler({ loader: "ts" }); +type SourceGraph = ReadonlyMap>; -interface SourceGraph { - readonly dependencies: ReadonlyMap>; - readonly files: readonly string[]; -} - -function admitSourceDependency( - importer: string, - specifier: string, - target: string, - files: ReadonlySet, -): string | undefined { - if (target === packageManifest) { - return undefined; - } - if (!target.startsWith(`${sourceRoot}${sep}`)) { - throw new Error( - `Source import resolves outside the source root: ${sourcePath(importer)} -> ${specifier}.`, - ); - } - if (target.endsWith(".json")) { - return undefined; - } - if (!files.has(target)) { - throw new Error(`Source import resolved to an unsupported file: ${sourcePath(target)}.`); +async function createSourceGraph(): Promise { + const entrypoints = await Array.fromAsync( + new Bun.Glob("src/**/*.ts").scan({ absolute: true, cwd: repositoryRoot, onlyFiles: true }), + ); + const metafile = ( + await Bun.build({ + allowUnresolved: [], + entrypoints, + metafile: true, + packages: "external", + root: repositoryRoot, + treeShaking: false, + }) + ).metafile!; + const inputs = new Set(Object.keys(metafile.inputs)); + const sourceFiles = new Set( + [...inputs].filter((path) => path.startsWith("src/") && path.endsWith(".ts")), + ); + const unsupportedInput = [...inputs].find( + (path) => + !sourceFiles.has(path) && + path !== "package.json" && + !(path.startsWith("src/") && path.endsWith(".json")), + ); + if (unsupportedInput !== undefined) { + throw new Error(`Source import resolved to an unsupported file: ${unsupportedInput}.`); } - return target; -} - -async function createSourceGraph(): Promise { - const files = ( - await Array.fromAsync( - new Bun.Glob("src/**/*.ts").scan({ absolute: true, cwd: repositoryRoot, onlyFiles: true }), - ) - ).toSorted(); - const fileSet = new Set(files); - const dependencies = new Map>(); - - await Promise.all( - files.map(async (file) => { - const source = (await Bun.file(file).text()).replace(/^#![^\n]*\n/, ""); - const resolved = new Set(); - - for (const dependency of transpiler.scanImports(source)) { - if (dependency.kind !== "import-statement") { - throw new Error(`Runtime module loaders are unsupported: ${sourcePath(file)}.`); - } - if (!dependency.path.startsWith(".")) { - continue; - } - - const target = Bun.resolveSync(dependency.path, dirname(file)); - const sourceDependency = admitSourceDependency(file, dependency.path, target, fileSet); - if (sourceDependency !== undefined) { - resolved.add(sourceDependency); - } + return new Map( + [...sourceFiles].map((file) => { + const imports = metafile.inputs[file]!.imports; + if (imports.some((dependency) => dependency.kind !== "import-statement")) { + throw new Error(`Runtime module loaders are unsupported: ${file}.`); } - - dependencies.set(file, resolved); + return [ + file, + new Set( + imports.map((dependency) => dependency.path).filter((path) => sourceFiles.has(path)), + ), + ]; }), ); - - return { dependencies, files }; -} - -function sourcePath(path: string): string { - return relative(repositoryRoot, path).split(sep).join("/"); } function isWithin(path: string, directory: string): boolean { - const root = resolve(sourceRoot, directory); - return path === root || path.startsWith(`${root}${sep}`); + return path.startsWith(`src/${directory}/`); } function isProviderRuntime(path: string): boolean { @@ -87,16 +58,17 @@ function isProviderRuntime(path: string): boolean { ); } -function reachableDependencies(graph: SourceGraph, file: string): ReadonlySet { - const reachable = new Set(); - const pending = [...(graph.dependencies.get(file) ?? [])]; - - for (const dependency of pending) { +function reachableDependencies( + graph: SourceGraph, + file: string, + reachable = new Set(), +): ReadonlySet { + for (const dependency of graph.get(file) ?? []) { if (reachable.has(dependency)) { continue; } reachable.add(dependency); - pending.push(...(graph.dependencies.get(dependency) ?? [])); + reachableDependencies(graph, dependency, reachable); } return reachable; @@ -105,7 +77,7 @@ function reachableDependencies(graph: SourceGraph, file: string): ReadonlySet ${sourcePath(dependency)}`); + violations.push(`${file} -> ${dependency}`); } } } @@ -136,7 +108,7 @@ function boundaryViolations(graph: SourceGraph): string[] { } function compositionViolations(graph: SourceGraph): string[] { - return graph.files + return [...graph.keys()] .filter((file) => !isWithin(file, "bin")) .filter((file) => { const dependencies = [...reachableDependencies(graph, file)]; @@ -146,21 +118,20 @@ function compositionViolations(graph: SourceGraph): string[] { dependencies.some((dependency) => isWithin(dependency, "infrastructure")) ); }) - .map(sourcePath) .toSorted(); } function sourceDirectory(path: string): string { - const parts = relative(sourceRoot, path).split(sep); + const parts = path.slice("src/".length).split("/"); return parts.length === 1 ? "(entrypoints)" : parts[0]!; } function bidirectionalDirectories(graph: SourceGraph): string[] { const edges = new Set(); - for (const file of graph.files) { + for (const file of graph.keys()) { const source = sourceDirectory(file); - for (const dependency of graph.dependencies.get(file) ?? []) { + for (const dependency of graph.get(file) ?? []) { const target = sourceDirectory(dependency); if (source !== target) { edges.add(`${source}\0${target}`); @@ -181,68 +152,43 @@ function bidirectionalDirectories(graph: SourceGraph): string[] { const graph = await createSourceGraph(); describe("source architecture", () => { - test("allows only source JSON leaves and the root package manifest outside the TS graph", () => { - const importer = resolve(sourceRoot, "runtime.ts"); - const files = new Set([importer]); - - expect( - admitSourceDependency(importer, "../package.json", packageManifest, files), - ).toBeUndefined(); - expect( - admitSourceDependency(importer, "./schema.json", resolve(sourceRoot, "schema.json"), files), - ).toBeUndefined(); - expect(() => - admitSourceDependency(importer, "./README.md", resolve(sourceRoot, "README.md"), files), - ).toThrow("unsupported file"); - expect(() => - admitSourceDependency( - importer, - "../schema.json", - resolve(repositoryRoot, "schema.json"), - files, - ), - ).toThrow("outside the source root"); - }); - test("keeps transitive contract, protocol, core, and provider dependencies inward", () => { expect(boundaryViolations(graph)).toEqual([]); - const core = resolve(sourceRoot, "core/audit.ts"); - const wrapper = resolve(sourceRoot, "audit/wrapper.ts"); - const provider = resolve(sourceRoot, "runtimes/openai/audit.ts"); + const core = "src/core/audit.ts"; + const wrapper = "src/audit/wrapper.ts"; + const provider = "src/runtimes/openai/audit.ts"; expect( - boundaryViolations({ - dependencies: new Map([ + boundaryViolations( + new Map([ [core, new Set([wrapper])], [wrapper, new Set([provider])], [provider, new Set()], ]), - files: [core, wrapper, provider], - }), + ), ).toEqual(["src/core/audit.ts -> src/runtimes/openai/audit.ts"]); }); test("keeps bin as the only transitive full runtime composition root", () => { expect(compositionViolations(graph)).toEqual([]); - const root = resolve(sourceRoot, "composition-audit.ts"); - const wrapper = resolve(sourceRoot, "audit/wrapper.ts"); - const core = resolve(sourceRoot, "core/audit.ts"); - const provider = resolve(sourceRoot, "runtimes/openai/audit.ts"); - const infrastructure = resolve(sourceRoot, "infrastructure/audit.ts"); + const root = "src/composition-audit.ts"; + const wrapper = "src/audit/wrapper.ts"; + const core = "src/core/audit.ts"; + const provider = "src/runtimes/openai/audit.ts"; + const infrastructure = "src/infrastructure/audit.ts"; expect( - compositionViolations({ - dependencies: new Map([ + compositionViolations( + new Map([ [root, new Set([wrapper])], [wrapper, new Set([core, provider, infrastructure])], [core, new Set()], [provider, new Set()], [infrastructure, new Set()], ]), - files: [root, wrapper, core, provider, infrastructure], - }), + ), ).toEqual(["src/audit/wrapper.ts", "src/composition-audit.ts"]); }); diff --git a/tests/claude-agent-sdk-driver-backend.test.ts b/tests/claude-agent-sdk-driver-backend.test.ts index e884fef..f925555 100644 --- a/tests/claude-agent-sdk-driver-backend.test.ts +++ b/tests/claude-agent-sdk-driver-backend.test.ts @@ -7,7 +7,10 @@ import type { WarmQuery, } from "@anthropic-ai/claude-agent-sdk"; -import { DriverTurnCancelledError } from "../src/core/driver-runtime-state"; +import { + DriverRuntimeStateMachine, + DriverTurnCancelledError, +} from "../src/core/driver-runtime-state"; import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; @@ -15,7 +18,13 @@ import type { DriverStartInput } from "../src/protocol/start"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { ClaudeAgentSdkDriverBackend } from "../src/runtimes/claude/agent-sdk-driver-backend"; import { registerClaudeTaskRetry } from "../src/runtimes/claude/agent-sdk-tasks"; -import { bootPayload, DRIVER_TEST_IDS } from "./driver-runtime-boundary-fixtures"; +import { settlePromiseWithTimeout } from "../src/utils/async"; +import { + bootPayload, + createDispatcher, + DRIVER_TEST_IDS, + FakeDriverRuntimeIo, +} from "./driver-runtime-boundary-fixtures"; const PREWARM_ENV = "AGENT_DRIVER_CLAUDE_PREWARM"; const previousPrewarm = process.env[PREWARM_ENV]; @@ -70,6 +79,13 @@ function errorResultMessage(): SDKMessage { } as unknown as SDKMessage; } +function cancelledResultMessage(): SDKMessage { + return { + ...errorResultMessage(), + terminal_reason: "aborted_tools", + } as SDKMessage; +} + function fakeQuery( messages: AsyncIterable | readonly SDKMessage[], close = () => {}, @@ -160,11 +176,11 @@ function createHarness( }); const backend = new ClaudeAgentSdkDriverBackend(payload, dependencies); const handleInput = backend.handleInput.bind(backend); - backend.handleInput = async (inputContext, input, runId) => { + backend.handleInput = async (inputContext, input, runId, signal) => { currentRunId ??= runId; try { - await handleInput(inputContext, input, runId); + await handleInput(inputContext, input, runId, signal); } finally { if (currentRunId === runId) { currentRunId = null; @@ -785,12 +801,15 @@ describe("Claude Agent SDK driver backend", () => { ); await optionsRequested.promise; + let stopping: Promise | null = null; if (action === "cancel") { await harness.backend.cancelActiveTurn(harness.context, "test.cancel"); } else { - await harness.backend.stop(harness.context, "test.stop", new AbortController().signal); + stopping = harness.backend.stop(harness.context, "test.stop", new AbortController().signal); + } + if (stopping !== null) { + await stopping; } - options.resolve({}); await expect(handling).rejects.toBeInstanceOf(DriverTurnCancelledError); expect(queryCalls).toBe(0); @@ -919,6 +938,106 @@ describe("Claude Agent SDK driver backend", () => { ).toBe(false); }); + test("bounds a stuck dequeued-result return after cancellation is claimed", async () => { + delete process.env[PREWARM_ENV]; + const releaseReturn = Promise.withResolvers(); + const returnStarted = Promise.withResolvers(); + let closeCalls = 0; + let innerReturnCalls = 0; + let outerReturnCalls = 0; + let turnSignal: AbortSignal | undefined; + const message = resultMessage(); + let innerMessagePending = true; + const inner = { + async next() { + if (innerMessagePending) { + innerMessagePending = false; + return { done: false as const, value: message }; + } + return { done: true as const, value: undefined }; + }, + async return() { + innerReturnCalls += 1; + await releaseReturn.promise; + return { done: true as const, value: undefined }; + }, + [Symbol.asyncIterator]() { + return this; + }, + }; + let outerMessagePending = true; + const query = { + close() { + closeCalls += 1; + }, + async interrupt() {}, + async next() { + if (outerMessagePending) { + outerMessagePending = false; + return { done: false as const, value: message }; + } + return { done: true as const, value: undefined }; + }, + async return() { + outerReturnCalls += 1; + returnStarted.resolve(); + await releaseReturn.promise; + return { done: true as const, value: undefined }; + }, + [Symbol.asyncIterator]() { + return inner; + }, + } as unknown as Query; + const harness = createHarness({ + createQueryOptions: async ({ abortController }) => { + turnSignal = abortController.signal; + return {}; + }, + query: () => query, + startup: async () => { + throw new Error("prewarm is disabled"); + }, + }); + const runCancellation = new AbortController(); + + try { + const handling = harness.backend.handleInput( + harness.context, + { text: "finish" }, + DRIVER_TEST_IDS.runId, + runCancellation.signal, + ); + await returnStarted.promise; + runCancellation.abort(new DriverTurnCancelledError("test.cancel")); + + expect( + await settlePromiseWithTimeout( + harness.backend.cancelActiveTurn(harness.context, "test.cancel"), + { label: "dequeued Claude cancellation", timeoutMs: 50 }, + ), + ).toMatchObject({ status: "completed" }); + expect(turnSignal?.aborted).toBe(true); + expect( + await settlePromiseWithTimeout(handling, { + label: "stuck Claude query return", + timeoutMs: 3_000, + }), + ).toMatchObject({ error: expect.any(DriverTurnCancelledError), status: "failed" }); + + expect(closeCalls).toBe(1); + expect(outerReturnCalls).toBe(1); + expect(innerReturnCalls).toBe(0); + expect( + harness.events.filter((event) => + ["run.cancelled", "run.completed", "run.failed"].includes(event.kind), + ), + ).toMatchObject([{ kind: "run.cancelled", runId: DRIVER_TEST_IDS.runId }]); + } finally { + releaseReturn.resolve(); + await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); + } + }, 6_000); + test("awaits query and process cleanup before publishing a completed terminal", async () => { delete process.env[PREWARM_ENV]; const cleanupStarted = Promise.withResolvers(); @@ -1101,9 +1220,7 @@ describe("Claude Agent SDK driver backend", () => { delete process.env[PREWARM_ENV]; const closed = Promise.withResolvers(); const cleanupStarted = Promise.withResolvers(); - const interrupted = Promise.withResolvers(); const releaseCleanup = Promise.withResolvers(); - const releaseInterrupt = Promise.withResolvers(); const releasePermissionDelivery = Promise.withResolvers(); const queryCreated = Promise.withResolvers(); const started = Promise.withResolvers(); @@ -1136,10 +1253,7 @@ describe("Claude Agent SDK driver backend", () => { closes += 1; closed.resolve(); }, - async () => { - interrupted.resolve(); - await releaseInterrupt.promise; - }, + async () => undefined, async () => { cleanupStarted.resolve(); await releaseCleanup.promise; @@ -1171,18 +1285,15 @@ describe("Claude Agent SDK driver backend", () => { await queryCreated.promise; const cancellation = harness.backend.cancelActiveTurn(harness.context, "test.cancel"); - await interrupted.promise; - expect(turnSignal?.aborted).toBe(false); - expect(closes).toBe(0); - releaseInterrupt.resolve(); + await cancellation; await cleanupStarted.promise; expect(turnSignal?.aborted).toBe(true); + expect(closes).toBe(1); expect(harness.events.some((event) => event.kind === "run.cancelled")).toBe(false); releaseCleanup.resolve(); await nextEventLoopTurn(); expect(harness.events.some((event) => event.kind === "run.cancelled")).toBe(false); releasePermissionDelivery.resolve(); - await cancellation; await expect(handling).rejects.toBeInstanceOf(DriverTurnCancelledError); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); @@ -1195,15 +1306,84 @@ describe("Claude Agent SDK driver backend", () => { expect(terminalOrder).toEqual(["permission.resolved", "run.cancelled"]); }); - test("stop joins an in-flight cancellation and propagates its cleanup failure", async () => { + test("stop waits for the cancelled terminal acknowledgement", async () => { + delete process.env[PREWARM_ENV]; + const closed = Promise.withResolvers(); + const releaseTerminal = Promise.withResolvers(); + const terminalEntered = Promise.withResolvers(); + const queryCreated = Promise.withResolvers(); + const harness = createHarness( + { + createQueryOptions: async () => ({}), + query: () => { + queryCreated.resolve(); + return fakeQuery( + (async function* () { + await closed.promise; + yield* [] as SDKMessage[]; + })(), + () => closed.resolve(), + ); + }, + startup: async () => { + throw new Error("prewarm is disabled"); + }, + }, + async (events) => { + if (events.some(({ kind }) => kind === "run.cancelled")) { + terminalEntered.resolve(); + await releaseTerminal.promise; + } + }, + ); + const handling = harness.backend.handleInput( + harness.context, + { text: "wait" }, + DRIVER_TEST_IDS.runId, + ); + void handling.catch(() => {}); + await queryCreated.promise; + let stopSettled = false; + const stopping = harness.backend.stop( + harness.context, + "test.stop", + new AbortController().signal, + ); + void stopping.then( + () => { + stopSettled = true; + }, + () => { + stopSettled = true; + }, + ); + + try { + await terminalEntered.promise; + await nextEventLoopTurn(); + expect(stopSettled).toBe(false); + releaseTerminal.resolve(); + await expect(Promise.all([stopping, handling])).rejects.toBeInstanceOf( + DriverTurnCancelledError, + ); + expect( + harness.events.filter(({ kind }) => + ["run.cancelled", "run.completed", "run.failed"].includes(kind), + ), + ).toMatchObject([{ kind: "run.cancelled", runId: DRIVER_TEST_IDS.runId }]); + } finally { + releaseTerminal.resolve(); + await Promise.allSettled([stopping, handling]); + } + }); + + test("stop owns in-flight cancellation cleanup and propagates its failure", async () => { delete process.env[PREWARM_ENV]; const closed = Promise.withResolvers(); const cleanupError = new Error("provider process cleanup failed"); const cleanupStarted = Promise.withResolvers(); - const interrupted = Promise.withResolvers(); const processExit = Promise.withResolvers(); const queryCreated = Promise.withResolvers(); - const releaseInterrupt = Promise.withResolvers(); let cancellationSettled = false; let stopSettled = false; const harness = createHarness({ @@ -1219,10 +1399,7 @@ describe("Claude Agent SDK driver backend", () => { yield* [] as SDKMessage[]; })(), () => closed.resolve(), - async () => { - interrupted.resolve(); - await releaseInterrupt.promise; - }, + async () => undefined, async () => cleanupStarted.resolve(), ); }, @@ -1246,7 +1423,7 @@ describe("Claude Agent SDK driver backend", () => { cancellationSettled = true; }, ); - await interrupted.promise; + await cleanupStarted.promise; const stopping = harness.backend.stop( harness.context, @@ -1262,19 +1439,13 @@ describe("Claude Agent SDK driver backend", () => { }, ); await nextEventLoopTurn(); - expect(cancellationSettled).toBe(false); - expect(stopSettled).toBe(false); - - releaseInterrupt.resolve(); - await cleanupStarted.promise; - await nextEventLoopTurn(); - expect(cancellationSettled).toBe(false); + expect(cancellationSettled).toBe(true); expect(stopSettled).toBe(false); const settled = Promise.allSettled([cancellation, stopping, handling]); processExit.reject(cleanupError); expect(await settled).toEqual([ - { reason: cleanupError, status: "rejected" }, + { status: "fulfilled", value: undefined }, { reason: cleanupError, status: "rejected" }, { reason: cleanupError, status: "rejected" }, ]); @@ -1326,6 +1497,236 @@ describe("Claude Agent SDK driver backend", () => { ).toMatchObject([{ kind: "run.completed", runId: DRIVER_TEST_IDS.runId }]); }); + test.each(["query cleanup", "terminal selection"] as const)( + "honors a socket cancellation claimed during %s", + async (window) => { + delete process.env[PREWARM_ENV]; + const windowEntered = Promise.withResolvers(); + const releaseWindow = Promise.withResolvers(); + const cancellationClaimed = Promise.withResolvers(); + const order: string[] = []; + const backend = new ClaudeAgentSdkDriverBackend( + { + ...bootPayload, + runtime: "claude-agent-sdk", + runtimeTransport: "claude-agent-sdk", + } as DriverStartInput, + { + createQueryOptions: async () => ({}), + query: () => + fakeQuery( + [resultMessage()], + () => {}, + async () => undefined, + async () => { + if (window === "query cleanup") { + windowEntered.resolve(); + await releaseWindow.promise; + } + order.push("cleanup"); + }, + ), + }, + ); + const socket = new FakeDriverRuntimeIo([ + { + commandId: "result-cleanup-input", + input: { text: "finish" }, + kind: "input.start", + requestId: "result-cleanup-request", + runId: DRIVER_TEST_IDS.runId, + }, + { + commandId: "result-cleanup-cancel", + kind: "turn.cancel", + reason: "test.cancel", + runId: DRIVER_TEST_IDS.runId, + }, + ]); + const nextCommand = socket.nextCommand.bind(socket); + socket.nextCommand = async (signal) => { + const command = await nextCommand(signal); + if (command?.kind === "turn.cancel") { + await windowEntered.promise; + } + return command; + }; + const registerRunTerminalBarrier = socket.registerRunTerminalBarrier.bind(socket); + socket.registerRunTerminalBarrier = (barrier) => + registerRunTerminalBarrier((events) => { + const pending = barrier(events); + if ( + window !== "terminal selection" || + !events.some(({ kind }) => kind === "run.completed") + ) { + return pending; + } + return Promise.resolve(pending).then(async () => { + windowEntered.resolve(); + await releaseWindow.promise; + }); + }); + const claimRunCancellation = socket.claimRunCancellation.bind(socket); + socket.claimRunCancellation = (ticket, reason) => { + const claim = claimRunCancellation(ticket, reason); + cancellationClaimed.resolve(); + return claim; + }; + const pushEvents = socket.pushEvents.bind(socket); + socket.pushEvents = async (input) => { + const result = await pushEvents(input); + const terminal = input.events.find(({ kind }) => + ["run.cancelled", "run.completed", "run.failed"].includes(kind), + ); + if (terminal !== undefined) { + order.push(terminal.kind); + } + return result; + }; + const runtimeState = new DriverRuntimeStateMachine("ready"); + const { dispatcher, logger, shutdownCalls } = createDispatcher({ + backend, + isShuttingDown: () => socket.isDrained(), + runtimeState, + }); + + const running = dispatcher.run(socket, logger); + await windowEntered.promise; + await cancellationClaimed.promise; + const claimedSnapshot = socket.runSnapshot(DRIVER_TEST_IDS.runId); + releaseWindow.resolve(); + await running; + + expect(claimedSnapshot).toMatchObject({ + cancellation: { reason: "test.cancel" }, + terminal: null, + }); + expect(order).toEqual(["cleanup", "run.cancelled"]); + expect(socket.updates).toEqual([ + { commandId: "result-cleanup-input", status: "accepted" }, + { commandId: "result-cleanup-cancel", status: "accepted" }, + { commandId: "result-cleanup-input", status: "cancelled" }, + { commandId: "result-cleanup-cancel", status: "completed" }, + ]); + expect(socket.failedRuns).toEqual([]); + expect(shutdownCalls).toEqual([]); + expect(runtimeState.status()).toBe("ready"); + const events = socket.pushedEvents.flatMap(({ events }) => events); + expect( + events + .filter(({ kind }) => kind === "message.completed" || kind === "message.cancelled") + .map(({ kind }) => kind), + ).toEqual(["message.completed"]); + expect(events.filter(({ kind }) => kind === "agent.tasks.replaced")).toHaveLength(1); + }, + ); + + test("settles a provider-aborted result as cancellation without failing the runtime", async () => { + delete process.env[PREWARM_ENV]; + const backend = new ClaudeAgentSdkDriverBackend( + { + ...bootPayload, + runtime: "claude-agent-sdk", + runtimeTransport: "claude-agent-sdk", + } as DriverStartInput, + { + createQueryOptions: async () => ({}), + query: () => fakeQuery([cancelledResultMessage()]), + }, + ); + const socket = new FakeDriverRuntimeIo([ + { + commandId: "provider-cancelled-input", + input: { text: "cancel" }, + kind: "input.start", + requestId: "provider-cancelled-request", + runId: DRIVER_TEST_IDS.runId, + }, + ]); + let cancellationClaims = 0; + const claimRunCancellation = socket.claimRunCancellation.bind(socket); + socket.claimRunCancellation = (ticket, reason) => { + cancellationClaims += 1; + return claimRunCancellation(ticket, reason); + }; + const runtimeState = new DriverRuntimeStateMachine("ready"); + const { dispatcher, logger } = createDispatcher({ + backend, + isShuttingDown: () => socket.isDrained() && socket.currentRunId() === null, + runtimeState, + }); + + await dispatcher.run(socket, logger); + + expect( + socket.pushedEvents + .flatMap(({ events }) => events) + .filter(({ kind }) => ["run.cancelled", "run.completed", "run.failed"].includes(kind)) + .map(({ kind }) => kind), + ).toEqual(["run.cancelled"]); + expect(socket.updates).toEqual([ + { commandId: "provider-cancelled-input", status: "accepted" }, + { commandId: "provider-cancelled-input", status: "cancelled" }, + ]); + expect(socket.failedRuns).toEqual([]); + expect(cancellationClaims).toBe(0); + expect(runtimeState.status()).toBe("ready"); + }); + + test("keeps a provider failure authoritative when cancellation is claimed during cleanup", async () => { + delete process.env[PREWARM_ENV]; + const cleanupEntered = Promise.withResolvers(); + const releaseCleanup = Promise.withResolvers(); + const payload = { + ...bootPayload, + runtime: "claude-agent-sdk", + runtimeTransport: "claude-agent-sdk", + } as DriverStartInput; + const backend = new ClaudeAgentSdkDriverBackend(payload, { + createQueryOptions: async () => ({}), + query: () => + fakeQuery( + [errorResultMessage()], + () => {}, + async () => undefined, + async () => { + cleanupEntered.resolve(); + await releaseCleanup.promise; + }, + ), + }); + const socket = new FakeDriverRuntimeIo([]); + const ticket = socket.beginRun(DRIVER_TEST_IDS.runId); + const context = createAgentDriverContext({ + eventSink: socket, + logger: createDisabledLogger(), + payload, + permission: { request: async () => "reject_once" }, + }); + + const handling = backend.handleInput( + context, + { text: "fail" }, + DRIVER_TEST_IDS.runId, + ticket.signal, + ); + await cleanupEntered.promise; + expect(socket.claimRunCancellation(ticket, "test.cancel")).toBe("claimed"); + releaseCleanup.resolve(); + + await expect(handling).rejects.toThrow("failed"); + expect(socket.runSnapshot(DRIVER_TEST_IDS.runId)?.terminal).toMatchObject({ + phase: "acked", + value: { status: "failed" }, + }); + expect( + socket.pushedEvents + .flatMap(({ events }) => events) + .filter(({ kind }) => ["run.cancelled", "run.completed", "run.failed"].includes(kind)) + .map(({ kind }) => kind), + ).toEqual(["run.failed"]); + }); + test.each([ { message: resultMessage(), terminal: "run.completed" }, { message: errorResultMessage(), terminal: "run.failed" }, diff --git a/tests/claude-agent-sdk-durability.test.ts b/tests/claude-agent-sdk-durability.test.ts index cfd5b85..d95cdf5 100644 --- a/tests/claude-agent-sdk-durability.test.ts +++ b/tests/claude-agent-sdk-durability.test.ts @@ -667,7 +667,7 @@ describe("Claude Agent SDK durable event boundaries", () => { await expect(translator.handleSdkMessage(context, messageStop, runId)).rejects.toThrow( "thought delivery failed", ); - await expect(translator.handleSdkMessage(context, messageStop, runId)).resolves.toBe(false); + await expect(translator.handleSdkMessage(context, messageStop, runId)).resolves.toBeNull(); expect(reasons.filter((reason) => reason === "driver.claude.thought.completed")).toHaveLength( 2, ); @@ -696,7 +696,7 @@ describe("Claude Agent SDK durable event boundaries", () => { await expect(translator.handleSdkMessage(context, fallback, runId)).rejects.toThrow( "tool retraction failed", ); - await expect(translator.handleSdkMessage(context, fallback, runId)).resolves.toBe(false); + await expect(translator.handleSdkMessage(context, fallback, runId)).resolves.toBeNull(); expect( replayedEvents.filter( (event) => event.kind === "message.cancelled" && payload(event)["reason"] === "superseded", diff --git a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts index 8df26ab..fccc4e1 100644 --- a/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-terminal.test.ts @@ -85,7 +85,7 @@ describe("Claude Agent SDK provider fixtures", () => { // Real SDK wire shape: every envelope (each stream chunk and the complete // assistant replay) carries a distinct uuid; only the API message id inside // message_start / assistant.message is stable across the whole message. - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -151,9 +151,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const startedEvents = events().filter((event) => event.kind === "message.started"); const completedEvents = events().filter((event) => event.kind === "message.completed"); @@ -182,7 +180,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("scopes interleaved subagent stream chunks away from the main message", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -231,9 +229,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const textByMessageId = new Map(); @@ -254,7 +250,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("does not promote a replayed stream-only message stop to canonical final", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -287,9 +283,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const liveText = events().flatMap((event) => { if (event.kind !== "message.delta" || !isRecord(event.payload)) { @@ -310,7 +304,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("materializes a result-only native resume completion", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { type: "message_stop" }, @@ -327,9 +321,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const translated = events(); const started = translated.filter((event) => event.kind === "message.started"); @@ -416,7 +408,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("retracts a superseded refusal before publishing its replacement", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { message: { @@ -452,9 +444,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const snapshots = events().flatMap((event) => { if (event.kind !== "message.added" || !isRecord(event.payload)) { @@ -494,7 +484,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("applies refusal fallback retractions idempotently to messages and tool results", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const fallback = { content: "Retrying with fallback model.", direction: "retry", @@ -573,9 +563,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const retractedMessages = events().filter( (event) => @@ -620,7 +608,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("keeps assistant envelopes distinct when they share an API message id", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { message: { content: [{ text: "first", type: "text" }], id: "shared-native" }, @@ -649,9 +637,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const snapshots = events().flatMap((event) => { if (event.kind !== "message.added" || !isRecord(event.payload)) { @@ -704,7 +690,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("does not let a late older assistant completion replace the final message", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -738,9 +724,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const runCompleted = events().find((event) => event.kind === "run.completed"); const payload = @@ -753,7 +737,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("fails closed when a newer assistant is still incomplete at result success", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { message: { @@ -780,9 +764,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const runCompleted = events().find((event) => event.kind === "run.completed"); const payload = @@ -795,7 +777,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("fails closed when an older full frame arrives after a newer incomplete message", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -822,9 +804,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const runCompleted = events().find((event) => event.kind === "run.completed"); const payload = @@ -841,7 +821,7 @@ describe("Claude Agent SDK provider fixtures", () => { // scope comes from the burst anchor: parallel tool blocks of one message // are told apart by content-block index, and a message_stop boundary // separates one message's thought stream from the next. - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const stream = (uuid: string, event: Record) => ({ event, type: "stream_event", uuid }) as unknown as SDKMessage; const messages = [ @@ -899,9 +879,7 @@ describe("Claude Agent SDK provider fixtures", () => { }), ]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const toolArguments = events().flatMap((event) => { if (event.kind !== "tool.call.updated" || !isRecord(event.payload)) { @@ -942,11 +920,9 @@ describe("Claude Agent SDK provider fixtures", () => { const fixture = readClaudeProviderFixtureCase( `./fixtures/providers/claude-agent-sdk/cases/${name}.json`, ); - const { context, events, nativeSessionIds, translator } = createHarness(); + const { events, handleMessages, nativeSessionIds } = createHarness(); - for (const message of fixture.messages) { - await translator.handleSdkMessage(context, message as SDKMessage, fixture.runId); - } + await handleMessages(fixture.messages as SDKMessage[], fixture.runId); expect(nativeSessionIds).toEqual(fixture.expectedNativeSessionIds); expect(normalizeClaudeEvents(events())).toEqual(fixture.expectedEvents); diff --git a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts index f64698f..47e4ccc 100644 --- a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts @@ -11,7 +11,7 @@ import { describe("Claude Agent SDK provider fixtures", () => { test("projects visible task activity without leaking task internals", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { description: "Inspect the repository", @@ -85,9 +85,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const taskEvents = events().filter((event) => event.kind === "agent.tasks.replaced"); expect(taskEvents).toHaveLength(2); @@ -228,7 +226,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("projects informational, local command, mirror failure, and conversation reset frames", async () => { - const { context, events, nativeSessionResets, translator } = createHarness(); + const { events, handleMessages, nativeSessionResets } = createHarness(); const messages = [ { content: "A stop hook blocked continuation.", @@ -267,9 +265,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); expect(events()).toContainEqual( expect.objectContaining({ @@ -409,7 +405,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("marks an SDK tool error as failed", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { message: { @@ -434,9 +430,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); expect(events()).toContainEqual( expect.objectContaining({ @@ -447,7 +441,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("classifies wrapper-level tool non-execution metadata", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { message: { @@ -482,9 +476,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); expect(events()).toContainEqual( expect.objectContaining({ @@ -568,7 +560,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("lets result permission denials override earlier tool terminals", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { message: { @@ -612,9 +604,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); for (const toolCallId of ["tool-completed", "tool-cancelled"]) { const statuses = events().flatMap((event) => { @@ -639,7 +629,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("rotates assistant identity across a tool boundary and marks the final message", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { message: { @@ -681,9 +671,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const textMessages = events().flatMap((event) => { if (event.kind !== "message.delta" || !isRecord(event.payload)) { @@ -709,7 +697,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("uses the complete assistant message to repair an incomplete stream snapshot", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -749,9 +737,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const runCompleted = events().find((event) => event.kind === "run.completed"); const payload = @@ -783,7 +769,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("drops thought and tool updates after their terminal events", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -865,9 +851,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const translated = events(); const thoughtCompletedIndex = translated.findIndex( @@ -893,7 +877,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("ignores streamed text arriving after its assistant message completed", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { message: { @@ -921,9 +905,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const translated = events(); const completed = translated.find((event) => event.kind === "message.completed"); @@ -954,7 +936,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("repairs streamed tool input with a lossless assistant snapshot", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -983,9 +965,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); expect( events().filter( @@ -1023,7 +1003,7 @@ describe("Claude Agent SDK provider fixtures", () => { test.each(["success", "error"] as const)( "closes every open item before a %s result terminal", async (outcome) => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -1071,9 +1051,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const translated = events(); const terminalIndex = translated.findIndex((event) => @@ -1105,7 +1083,7 @@ describe("Claude Agent SDK provider fixtures", () => { // envelope uuid, and the aggregated assistant envelope (with the native // message id) arrives before message_stop. The reply must stay one // message instead of rendering as "P" / "ong. …" / full-text duplicates. - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -1153,9 +1131,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const translated = events(); const textMessages = translated.flatMap((event) => { @@ -1201,7 +1177,7 @@ describe("Claude Agent SDK provider fixtures", () => { test("anchors uuid-fractured stream fragments to one closed assistant message", async () => { // One scope streams one message at a time; per-envelope uuids must not // fracture a burst whose message_start frame was lost (YEF-884). - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -1231,9 +1207,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const translated = events(); const started = translated @@ -1258,7 +1232,7 @@ describe("Claude Agent SDK provider fixtures", () => { // envelope uuid; the aggregated assistant envelope that follows in the // same scope is that burst's own aggregation and must not mint a // duplicate message (YEF-884). - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -1290,9 +1264,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const textMessages = events().flatMap((event) => { if (event.kind !== "message.delta" || !isRecord(event.payload)) { @@ -1322,7 +1294,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("keeps a confirmed streamed assistant replay on its original message", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const assistant = { message: { content: [{ text: "canonical", type: "text" }], @@ -1365,9 +1337,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const started = events().filter((event) => event.kind === "message.started"); const completed = events().filter((event) => event.kind === "message.completed"); @@ -1388,7 +1358,7 @@ describe("Claude Agent SDK provider fixtures", () => { }); test("keeps distinct live assistant envelopes that share a native message id", async () => { - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { message: { id: "shared-native" }, type: "message_start" }, @@ -1431,9 +1401,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const snapshots = events().filter((event) => event.kind === "message.added"); const messageIds = snapshots.flatMap((event) => @@ -1456,7 +1424,7 @@ describe("Claude Agent SDK provider fixtures", () => { // A message_start frame proves the streamed message's native id, so an // assistant envelope with a different native id is a genuinely separate // message even when the text repeats. - const { context, events, translator } = createHarness(); + const { events, handleMessages } = createHarness(); const messages = [ { event: { @@ -1497,9 +1465,7 @@ describe("Claude Agent SDK provider fixtures", () => { }, ] as unknown as SDKMessage[]; - for (const message of messages) { - await translator.handleSdkMessage(context, message, "run-1" as RunId); - } + await handleMessages(messages); const textMessages = events().flatMap((event) => { if (event.kind !== "message.delta" || !isRecord(event.payload)) { diff --git a/tests/claude-agent-sdk-test-helpers.ts b/tests/claude-agent-sdk-test-helpers.ts index 13c4551..ed15237 100644 --- a/tests/claude-agent-sdk-test-helpers.ts +++ b/tests/claude-agent-sdk-test-helpers.ts @@ -1,13 +1,15 @@ +import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; + import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { createAgentDriverContext } from "../src/core/agent-driver-backend"; import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import type { RunId } from "../src/protocol/id"; import { ClaudeAgentSdkMessageTranslator } from "../src/runtimes/claude/agent-sdk-message-translator"; -import { isRecord } from "../src/runtimes/claude/agent-sdk-json"; import { driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; export { isRecord } from "../src/runtimes/claude/agent-sdk-json"; +export { messageText } from "./driver-event-test-helpers"; export function createClaudeAgentSdkHarness( publicToolCallId: (nativeToolCallId: string) => string = (id) => id, @@ -40,42 +42,21 @@ export function createClaudeAgentSdkHarness( }, sessionId: context.payload.execution.run.sessionId, }); + const handleMessages = async ( + messages: readonly SDKMessage[], + runId: RunId = "run-1" as RunId, + ): Promise => { + for (const message of messages) { + await translator.handleSdkMessage(context, message, runId); + } + }; return { context, events: () => driverEvents, + handleMessages, nativeSessionIds, nativeSessionResets, translator, }; } - -export function messageText(events: readonly DriverEventInput[], messageId: unknown): string { - let text = ""; - - for (const event of events) { - if (!isRecord(event.payload) || event.payload["messageId"] !== messageId) { - continue; - } - - if (event.kind === "message.added") { - const content = event.payload["content"]; - text = Array.isArray(content) - ? content - .flatMap((block) => - isRecord(block) && typeof block["text"] === "string" ? [block["text"]] : [], - ) - .join("") - : typeof content === "string" - ? content - : ""; - } else if ( - event.kind === "message.delta" && - typeof event.payload["contentDelta"] === "string" - ) { - text += event.payload["contentDelta"]; - } - } - - return text; -} diff --git a/tests/driver-artifact-contract.test.ts b/tests/driver-artifact-contract.test.ts index e72f5c9..93e006a 100644 --- a/tests/driver-artifact-contract.test.ts +++ b/tests/driver-artifact-contract.test.ts @@ -1,41 +1,11 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; +import environmentPackageManagers from "../environment-package-managers.json" with { type: "json" }; +import packageJson from "../package.json" with { type: "json" }; import { AGENT_DRIVER_VERSION } from "../src/core/version"; import viteConfig from "../vite.config"; -interface DriverPackageExportTarget { - readonly default?: string; - readonly types?: string; -} - -interface DriverPackageJson { - readonly bin?: Record; - readonly bugs?: { readonly url?: string }; - readonly dependencies?: Record; - readonly devDependencies?: Record; - readonly description?: string; - readonly engines?: Record; - readonly exports?: Record; - readonly files?: readonly string[]; - readonly homepage?: string; - readonly license?: string; - readonly name?: string; - readonly packageManager?: string; - readonly private?: boolean; - readonly publishConfig?: Record; - readonly repository?: { readonly type?: string; readonly url?: string }; - readonly scripts?: Record; - readonly types?: string; - readonly type?: string; - readonly version?: string; -} - -interface EnvironmentPackageManagerManifest { - readonly managers?: readonly string[]; - readonly schemaVersion?: number; -} - interface WorkflowStep { readonly env?: Record; readonly name?: string; @@ -75,16 +45,6 @@ function readText(path: string): string { return readFileSync(new URL(path, import.meta.url), "utf8"); } -function readDriverPackageJson(): DriverPackageJson { - return JSON.parse(readText("../package.json")) as DriverPackageJson; -} - -function readEnvironmentPackageManagerManifest(): EnvironmentPackageManagerManifest { - return JSON.parse( - readText("../environment-package-managers.json"), - ) as EnvironmentPackageManagerManifest; -} - function readWorkflow(path: string): Workflow { return Bun.YAML.parse(readText(path)) as Workflow; } @@ -116,8 +76,6 @@ function workflowStep(job: WorkflowJob, name: string): WorkflowStep { describe("driver artifact contract", () => { test("uses the public mosoo package identity", () => { - const packageJson = readDriverPackageJson(); - expect(packageJson.name).toBe("@mosoo/agent-driver"); expect(packageJson.private).toBe(false); expect(packageJson.version).toBe(AGENT_DRIVER_VERSION); @@ -129,7 +87,7 @@ describe("driver artifact contract", () => { type: "git", url: "git+https://github.com/langgenius/mosoo-agent-driver.git", }); - expect(packageJson.bugs?.url).toBe("https://github.com/langgenius/mosoo-agent-driver/issues"); + expect(packageJson.bugs.url).toBe("https://github.com/langgenius/mosoo-agent-driver/issues"); expect(packageJson.homepage).toBe("https://github.com/langgenius/mosoo-agent-driver"); expect(packageJson.publishConfig).toEqual({ access: "public", @@ -143,13 +101,9 @@ describe("driver artifact contract", () => { }); test("keeps public package entries separate from process internals", () => { - const packageJson = readDriverPackageJson(); - expect(packageJson.type).toBe("module"); - expect(Object.keys(packageJson.exports ?? {}).toSorted()).toEqual( - [...PUBLIC_EXPORTS].toSorted(), - ); - expect(packageJson.exports?.["."]).toEqual({ + expect(Object.keys(packageJson.exports).toSorted()).toEqual([...PUBLIC_EXPORTS].toSorted()); + expect(packageJson.exports["."]).toEqual({ default: "./src/index.ts", types: "./dist/types/index.d.ts", }); @@ -157,8 +111,7 @@ describe("driver artifact contract", () => { }); test("builds declarations for exactly the public package entries", () => { - const packageJson = readDriverPackageJson(); - const targets = Object.values(packageJson.exports ?? {}); + const targets = Object.values(packageJson.exports); const sources = [ ...new Bun.Glob("src/*.ts").scanSync({ cwd: new URL("../", import.meta.url).pathname, @@ -173,16 +126,15 @@ describe("driver artifact contract", () => { outDir: "dist/types", platform: "neutral", }); - expect(targets.map(({ default: source }) => source?.slice(2)).toSorted()).toEqual(sources); + expect(targets.map(({ default: source }) => source.slice(2)).toSorted()).toEqual(sources); for (const { default: source, types } of targets) { expect(source).toMatch(/^\.\/src\/.+\.ts$/); - expect(types).toBe(source?.replace("./src/", "./dist/types/").replace(/\.ts$/, ".d.ts")); + expect(types).toBe(source.replace("./src/", "./dist/types/").replace(/\.ts$/, ".d.ts")); expect(source).not.toContain("/generated"); } }); test("uses package.json as the runtime version source", () => { - const packageJson = readDriverPackageJson(); const runtimeVersionSources = [ "../src/bin/driver-process.ts", "../src/runtimes/acp/acp-driver-backend.ts", @@ -199,13 +151,12 @@ describe("driver artifact contract", () => { }); test("pins container runtimes to package dependency versions", () => { - const packageJson = readDriverPackageJson(); const containerfile = readText("../Containerfile"); const versions = { - BUN_VERSION: packageJson.packageManager?.replace("bun@", ""), - CLAUDE_AGENT_SDK_VERSION: packageJson.dependencies?.["@anthropic-ai/claude-agent-sdk"], - OPENAI_RUNTIME_VERSION: packageJson.devDependencies?.["@openai/codex"], - OPENCODE_VERSION: packageJson.devDependencies?.["opencode-ai"], + BUN_VERSION: packageJson.packageManager.replace("bun@", ""), + CLAUDE_AGENT_SDK_VERSION: packageJson.dependencies["@anthropic-ai/claude-agent-sdk"], + OPENAI_RUNTIME_VERSION: packageJson.devDependencies["@openai/codex"], + OPENCODE_VERSION: packageJson.devDependencies["opencode-ai"], }; expect(readContainerArguments()).toMatchObject(versions); @@ -229,7 +180,7 @@ describe("driver artifact contract", () => { }); test("builds and tests the packed process artifact", () => { - const scripts = readDriverPackageJson().scripts ?? {}; + const scripts = packageJson.scripts; const containerfile = readText("../Containerfile"); expect(scripts["build"]).toContain("src/bin/driver.ts"); @@ -263,8 +214,6 @@ describe("driver artifact contract", () => { const imageBuildStep = workflowStep(buildJob, "Build image"); const imageRun = workflowStep(imageJob, "Publish versioned image").run ?? ""; const npmRun = workflowStep(npmJob, "Publish package").run ?? ""; - const buildRun = - buildJob.steps?.flatMap(({ run }) => (run === undefined ? [] : [run])).join("\n") ?? ""; const latestRun = latestJob.steps?.flatMap(({ run }) => (run === undefined ? [] : [run])).join("\n") ?? ""; const actionUses = [prWorkflow, releaseWorkflow].flatMap((workflow) => @@ -292,23 +241,22 @@ describe("driver artifact contract", () => { 'git merge-base --is-ancestor "${GITHUB_SHA}" origin/main', ]) expect(verifyRun).toContain(marker); - for (const marker of [ - "npm pack --ignore-scripts", - "declarations=(packed/dist/types/**/*.d.ts)", - "bun test tests/driver-artifact-mcp.test.ts", - "buildah build", + expect(packStep.run).toContain("npm pack --ignore-scripts"); + expect(mcpStep.run).toContain("declarations=(packed/dist/types/**/*.d.ts)"); + expect(imageBuildStep.run).toContain("buildah build"); + expect(workflowStep(buildJob, "Test image environment").run).toContain( "podman run --pull=never --rm", - ]) - expect(buildRun).toContain(marker); + ); for (const marker of [ - "remote_digest=", + "skopeo inspect --format '{{.Digest}}' \"docker://$IMAGE:$VERSION\"", 'if [[ "$remote_digest" != "$EXPECTED_DIGEST" ]]', "skopeo copy --preserve-digests", "gh attestation verify", ]) expect(imageRun).toContain(marker); for (const marker of [ - "remote_integrity=", + 'npm view "$package_name@$VERSION" dist.integrity', + 'if [[ "$remote_integrity" != "$local_integrity" ]]', 'npm view "$package_name@>$VERSION"', 'npm publish "$tarball" --ignore-scripts --provenance', ]) @@ -343,17 +291,14 @@ describe("driver artifact contract", () => { }); test("declares writable Environment package managers", () => { - const manifest = readEnvironmentPackageManagerManifest(); - - expect(manifest).toEqual({ + expect(environmentPackageManagers).toEqual({ managers: ["npm", "pip"], schemaVersion: 1, }); }); test("keeps the standalone package independent of Mosoo workspace packages", () => { - const packageJson = readDriverPackageJson(); - const deps = Object.keys(packageJson.dependencies ?? {}); + const deps = Object.keys(packageJson.dependencies); expect(deps.filter((dependency) => dependency.startsWith("@mosoo/"))).toEqual([]); }); diff --git a/tests/driver-artifact-live.test.ts b/tests/driver-artifact-live.test.ts index a2ba906..87c1843 100644 --- a/tests/driver-artifact-live.test.ts +++ b/tests/driver-artifact-live.test.ts @@ -13,6 +13,7 @@ import { type DriverArtifactBootPayload, type DriverArtifactTestEvent, } from "./driver-artifact-test-controller"; +import { messageText } from "./driver-event-test-helpers"; const LIVE_START_TIMEOUT_MS = 120_000; const LIVE_TURN_TIMEOUT_MS = 180_000; @@ -382,44 +383,6 @@ function payloadRecord(event: DriverArtifactTestEvent): Record : {}; } -function messageText(events: readonly DriverArtifactTestEvent[], messageId: string): string { - let text = ""; - - for (const event of events) { - const payload = payloadRecord(event); - if (payload["messageId"] !== messageId) { - continue; - } - - if (event.kind === "message.delta" && typeof payload["contentDelta"] === "string") { - text += payload["contentDelta"]; - continue; - } - - if (event.kind !== "message.added") { - continue; - } - - const content = payload["content"]; - text = - typeof content === "string" - ? content - : Array.isArray(content) - ? content - .flatMap((block) => { - const value = - typeof block === "object" && block !== null && !Array.isArray(block) - ? (block as Record)["text"] - : null; - return typeof value === "string" ? [value] : []; - }) - .join("") - : text; - } - - return text; -} - function eventText(events: readonly DriverArtifactTestEvent[]): string { const finalMessageId = events .filter((event) => event.kind === "run.completed") diff --git a/tests/driver-artifact-test-controller.test.ts b/tests/driver-artifact-test-controller.test.ts index 63239ec..d12cd9b 100644 --- a/tests/driver-artifact-test-controller.test.ts +++ b/tests/driver-artifact-test-controller.test.ts @@ -11,9 +11,7 @@ import { } from "./driver-artifact-test-controller"; import { driverRuntimeRpcSchemas, - parseDriverCommandUpdateInput, parseDriverFailureInput, - parseDriverHelloInput, parseDriverLogBatchInput, } from "../src/protocol/orpc"; @@ -160,7 +158,6 @@ describe("driver artifact test controller", () => { const diagnostics = controller.diagnostics(); expect(diagnostics).toContain('"message": "transport exploded: [redacted]"'); expect(diagnostics).toContain('"stage": "session/new"'); - expect(diagnostics).not.toContain(secret); expect(diagnostics).not.toContain(JSON.stringify(secret).slice(1, -1)); await controller.dispose(); }); @@ -255,96 +252,7 @@ describe("driver artifact test controller", () => { expect(disposeError.message).not.toContain(secret); }); - test("validates structured RPC inputs with the production parsers", () => { - const hello = parseDriverHelloInput({ - capabilities: [{ details: undefined, id: "input_start", status: "supported", version: 1 }], - driverVersion: "test", - pid: 1, - protocolVersion: DRIVER_PROTOCOL_VERSION, - runtime: "acp-fallback", - startedAt: "now", - }); - expect(Object.hasOwn(hello.capabilities[0]!, "details")).toBeFalse(); - expect(() => - parseDriverHelloInput({ - capabilities: [], - driverVersion: "test", - ignored: true, - pid: 1, - protocolVersion: DRIVER_PROTOCOL_VERSION, - runtime: "acp-fallback", - startedAt: "now", - }), - ).toThrow("ignored"); - expect(() => - parseDriverHelloInput({ - capabilities: [], - driverVersion: "test", - pid: 0, - protocolVersion: DRIVER_PROTOCOL_VERSION, - runtime: "acp-fallback", - startedAt: "now", - }), - ).toThrow("pid must be a positive safe integer"); - expect(() => - parseDriverCommandUpdateInput({ - commandId: "command-1", - driverInstanceId: "driver-1", - status: "invented", - }), - ).toThrow("accepted' | 'cancelled' | 'completed' | 'failed"); - const completed = parseDriverCommandUpdateInput({ - commandId: "command-1", - driverInstanceId: "driver-1", - result: { - isError: true, - outputText: "failed", - requestId: "request-1", - serverId: "server-1", - toolName: "tool-1", - }, - status: "completed", - }); - expect(completed.status === "completed" ? completed.result : undefined).toMatchObject({ - isError: true, - }); - expect(() => - parseDriverCommandUpdateInput({ - commandId: "command-1", - driverInstanceId: "driver-1", - result: { outputText: "partial", requestId: "request-1" }, - status: "completed", - }), - ).toThrow(); - expect(() => - parseDriverCommandUpdateInput({ - commandId: "command-1", - driverInstanceId: "driver-1", - result: { isError: true, requestId: "request-1" }, - status: "completed", - }), - ).toThrow(); - expect(() => - parseDriverCommandUpdateInput({ - commandId: "command-1", - driverInstanceId: "driver-1", - result: { outputText: undefined, requestId: "request-1" }, - status: "completed", - }), - ).toThrow(); - expect(() => - parseDriverHelloInput({ - capabilities: [ - { id: "input_start", status: "supported", version: 1 }, - { id: "input_start", status: "unsupported", version: 1 }, - ], - driverVersion: "test", - pid: 1, - protocolVersion: DRIVER_PROTOCOL_VERSION, - runtime: "acp-fallback", - startedAt: "now", - }), - ).toThrow("capabilities must not contain duplicate ids"); + test("validates structured RPC input edge cases", () => { expect(() => parseDriverFailureInput({ driverInstanceId: "driver-1", diff --git a/tests/driver-artifact-test-controller.ts b/tests/driver-artifact-test-controller.ts index 587b062..cae5e65 100644 --- a/tests/driver-artifact-test-controller.ts +++ b/tests/driver-artifact-test-controller.ts @@ -18,10 +18,12 @@ import { parseDriverLogBatchInput, parseDriverNextCommandInput, parseDriverReadyInput, + type DriverEventReceipt, type DriverLogEntry, } from "../src/protocol/orpc"; import type { DriverCapability, + DriverCommandUpdate, McpExecuteCommandResult, McpExternalToolEffectState, RuntimeCommand, @@ -47,13 +49,6 @@ export function expectedDriverCapabilities(runtime: string): readonly DriverCapa }); } -export interface DriverArtifactTestCommandUpdate { - readonly commandId: string; - readonly error?: unknown; - readonly result?: unknown; - readonly status: string; -} - export type DriverArtifactTestCommand = RuntimeCommand; export interface DriverArtifactEventIngressGate { @@ -323,15 +318,10 @@ function readSameUserLinuxIdentity(pid: number): LinuxProcessIdentity | null { export class DriverArtifactTestController { readonly #bootPayload: DriverArtifactBootPayload; readonly #commands: DriverArtifactTestCommand[] = []; - readonly #commandUpdates: DriverArtifactTestCommandUpdate[] = []; + readonly #commandUpdates: DriverCommandUpdate[] = []; readonly #eventIngressGates = new Set(); readonly #eventIngressObservers = new Set<(event: DriverArtifactTestEvent) => void>(); - readonly #eventReceipts = new Map< - string, - { - readonly receipt: { readonly eventId: string; readonly seq: number; readonly type: string }; - } - >(); + readonly #eventReceipts = new Map(); readonly #events: DriverArtifactTestEvent[] = []; readonly #externalToolEffects = new Map(); readonly #expectedCapabilities: readonly DriverCapability[] | undefined; @@ -437,7 +427,7 @@ export class DriverArtifactTestController { return this.#events; } - get commandUpdates(): readonly DriverArtifactTestCommandUpdate[] { + get commandUpdates(): readonly DriverCommandUpdate[] { return this.#commandUpdates; } @@ -662,7 +652,7 @@ export class DriverArtifactTestController { commandId: string, timeoutMs: number, fromIndex = 0, - ): Promise { + ): Promise { return this.#waitFor( () => this.#commandUpdates @@ -677,11 +667,11 @@ export class DriverArtifactTestController { } async waitForCommandUpdate( - predicate: (update: DriverArtifactTestCommandUpdate) => boolean, + predicate: (update: DriverCommandUpdate) => boolean, fromIndex: number, timeoutMs: number, label: string, - ): Promise { + ): Promise { return this.#waitFor( () => this.#commandUpdates.slice(fromIndex).find(predicate), label, @@ -965,10 +955,10 @@ export class DriverArtifactTestController { const existing = this.#eventReceipts.get(envelope.eventId); if (existing !== undefined) { - if (existing.receipt.type !== envelope.event.kind) { + if (existing.type !== envelope.event.kind) { throw new Error(`Driver reused event ID ${envelope.eventId} with a changed type.`); } - return existing.receipt; + return existing; } const { event } = envelope; @@ -1020,7 +1010,7 @@ export class DriverArtifactTestController { seq: this.#nextEventSeq, type: envelope.event.kind, }; - this.#eventReceipts.set(envelope.eventId, { receipt }); + this.#eventReceipts.set(envelope.eventId, receipt); return receipt; }); await Promise.all(ingressWaits); @@ -1033,23 +1023,9 @@ export class DriverArtifactTestController { return { ok: true }; } case "/driver/commandUpdate": { - const update = parseDriverCommandUpdateInput(input); - this.#assertDriverInstanceId(update.driverInstanceId); - this.#commandUpdates.push( - update.status === "failed" - ? { - commandId: update.commandId, - error: update.error, - status: update.status, - } - : update.status === "completed" - ? { - commandId: update.commandId, - ...(update.result === undefined ? {} : { result: update.result }), - status: update.status, - } - : { commandId: update.commandId, status: update.status }, - ); + const { driverInstanceId, ...update } = parseDriverCommandUpdateInput(input); + this.#assertDriverInstanceId(driverInstanceId); + this.#commandUpdates.push(update); return { ok: true }; } case "/driver/observeExternalToolEffect": { diff --git a/tests/driver-event-publisher-admission.test.ts b/tests/driver-event-publisher-admission.test.ts index dd8dd22..e0cec52 100644 --- a/tests/driver-event-publisher-admission.test.ts +++ b/tests/driver-event-publisher-admission.test.ts @@ -10,7 +10,10 @@ import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; import type { RunId } from "../src/protocol/id"; import type { DriverEventBatchOutput } from "../src/protocol/orpc"; -import { DriverEventPublisher } from "../src/runtimes/driver-event-publisher"; +import { + DriverCompletedTerminalSupersededError, + DriverEventPublisher, +} from "../src/runtimes/driver-event-publisher"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; import { createContext, createDelta, createEvent, kinds } from "./driver-event-publisher-fixture"; @@ -716,6 +719,164 @@ describe("DriverEventPublisher", () => { expect(kinds([accepted])).toEqual([["message.completed", "run.completed"]]); }); + test("lets cancellation replace only the unselected completed terminal", async () => { + const closureEntered = Promise.withResolvers(); + const releaseClosure = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + let closureAttempts = 0; + let seq = 0; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + + if (events[0]?.kind === "message.completed" && ++closureAttempts <= 2) { + if (closureAttempts === 1) { + closureEntered.resolve(); + await releaseClosure.promise; + } + throw new Error(`closure outcome unknown ${closureAttempts}`); + } + + return acceptEvents(events, () => (seq += 1)); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const cancellation = new Error("cancellation won"); + const controller = new AbortController(); + const closure = createEvent("message.completed"); + const completed = createRunTerminal("run.completed"); + const first = publisher.pushTerminal( + context, + "terminal", + [closure], + completed, + controller.signal, + ); + + await closureEntered.promise; + controller.abort(cancellation); + releaseClosure.resolve(); + await expect(first).rejects.toThrow("closure outcome unknown 2"); + await expect( + publisher.pushTerminal(context, "terminal.retry", [closure], completed, controller.signal), + ).rejects.toBeInstanceOf(DriverCompletedTerminalSupersededError); + await publisher.pushTerminal( + context, + "terminal.cancelled", + [], + createRunTerminal("run.cancelled"), + ); + + expect(kinds(attempts)).toEqual([ + ["message.completed"], + ["message.completed"], + ["message.completed"], + ["run.cancelled"], + ]); + expect(new Set(attempts.slice(0, 3).map(([event]) => event?.sourceEventId)).size).toBe(1); + }); + + test("does not start a completed settlement after cancellation was claimed", async () => { + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const cancellation = new Error("cancellation already won"); + const controller = new AbortController(); + controller.abort(cancellation); + + expect(() => + publisher.pushTerminal( + context, + "completed", + [createEvent("message.completed")], + createRunTerminal("run.completed"), + controller.signal, + ), + ).toThrow(cancellation); + await publisher.pushTerminal(context, "cancelled", [], createRunTerminal("run.cancelled")); + + expect(kinds(attempts)).toEqual([["run.cancelled"]]); + }); + + test("aborts an in-flight unselected completed terminal", async () => { + const terminalEntered = Promise.withResolvers(); + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events, signal) => { + attempts.push(events); + if (events[0]?.kind === "run.completed") { + terminalEntered.resolve(); + await new Promise((_resolve, reject) => + signal?.addEventListener("abort", () => reject(signal.reason), { once: true }), + ); + } + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const cancellation = new Error("cancellation won during delivery"); + const controller = new AbortController(); + const completed = publisher.pushTerminal( + context, + "completed", + [createEvent("message.completed")], + createRunTerminal("run.completed"), + controller.signal, + ); + + await terminalEntered.promise; + controller.abort(cancellation); + const error = await completed.catch((reason: unknown) => reason); + expect(error).toBeInstanceOf(DriverCompletedTerminalSupersededError); + expect((error as Error).cause).toBe(cancellation); + await publisher.pushTerminal(context, "cancelled", [], createRunTerminal("run.cancelled")); + + expect(kinds(attempts)).toEqual([["message.completed"], ["run.completed"], ["run.cancelled"]]); + }); + + test("releases a failed unselected completion when cancellation arrives later", async () => { + const attempts: DriverEventInput[][] = []; + const context = createContext({ + pushEvents: async (events) => { + attempts.push(events); + if (events[0]?.kind === "run.completed") { + throw new Error("completion outcome unknown"); + } + return acceptEvents(events); + }, + }); + const publisher = new DriverEventPublisher("openai-runtime", () => "session-ref"); + const controller = new AbortController(); + + await expect( + publisher.pushTerminal( + context, + "completed", + [], + createRunTerminal("run.completed"), + controller.signal, + ), + ).rejects.toThrow("completion outcome unknown"); + controller.abort(new Error("cancellation won")); + await expect( + publisher.pushTerminal( + context, + "completed.retry", + [], + createRunTerminal("run.completed"), + controller.signal, + ), + ).rejects.toBeInstanceOf(DriverCompletedTerminalSupersededError); + await publisher.pushTerminal(context, "cancelled", [], createRunTerminal("run.cancelled")); + + expect(kinds(attempts)).toEqual([["run.completed"], ["run.completed"], ["run.cancelled"]]); + }); + test("joins one in-flight terminal operation and rejects a different occurrence", async () => { const entered = Promise.withResolvers(); const release = Promise.withResolvers(); diff --git a/tests/driver-event-test-helpers.ts b/tests/driver-event-test-helpers.ts new file mode 100644 index 0000000..e294cc5 --- /dev/null +++ b/tests/driver-event-test-helpers.ts @@ -0,0 +1,44 @@ +interface DriverEventLike { + readonly kind: string; + readonly payload: unknown; +} + +function record(value: unknown): Record | null { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : null; +} + +export function messageText(events: readonly DriverEventLike[], messageId: unknown): string { + let text = ""; + + for (const event of events) { + const payload = record(event.payload); + if (payload === null || payload["messageId"] !== messageId) { + continue; + } + + if (event.kind === "message.delta" && typeof payload["contentDelta"] === "string") { + text += payload["contentDelta"]; + continue; + } + + if (event.kind !== "message.added") { + continue; + } + + const content = payload["content"]; + if (typeof content === "string") { + text = content; + } else if (Array.isArray(content)) { + text = content + .flatMap((block) => { + const value = record(block)?.["text"]; + return typeof value === "string" ? [value] : []; + }) + .join(""); + } + } + + return text; +} diff --git a/tests/driver-rpc-wire-v3.test.ts b/tests/driver-rpc-wire-v3.test.ts index f85a59a..ac55cee 100644 --- a/tests/driver-rpc-wire-v3.test.ts +++ b/tests/driver-rpc-wire-v3.test.ts @@ -97,31 +97,33 @@ function textFieldAtJsonSize(targetBytes: number, create: (text: string) } describe("Driver RPC wire v3", () => { + const rpc = driverRuntimeRpcSchemas.driver; + + test("omits explicit undefined capability details", () => { + const parsed = rpc.hello.input.parse( + helloInput({ + capabilities: [{ details: undefined, id: "input_start", status: "supported", version: 1 }], + }), + ); + + expect(Object.hasOwn(parsed.capabilities[0]!, "details")).toBeFalse(); + }); + test.each([2, 4] as const)("rejects protocol version %d", (protocolVersion) => { - expect( - driverRuntimeRpcSchemas.driver.hello.input.safeParse(helloInput({ protocolVersion })).success, - ).toBeFalse(); + expect(rpc.hello.input.safeParse(helloInput({ protocolVersion })).success).toBeFalse(); }); test.each(invalidPositiveSafeIntegers)( "rejects %p at every positive safe-integer field", (value) => { + expect(rpc.hello.input.safeParse(helloInput({ pid: value })).success).toBeFalse(); + expect(rpc.heartbeat.input.safeParse(heartbeatInput(value)).success).toBeFalse(); + expect(rpc.ready.input.safeParse(readyInput(value)).success).toBe(false); expect( - driverRuntimeRpcSchemas.driver.hello.input.safeParse(helloInput({ pid: value })).success, + rpc.hello.output.safeParse(helloOutput({ eventBatchMaxSize: value })).success, ).toBeFalse(); expect( - driverRuntimeRpcSchemas.driver.heartbeat.input.safeParse(heartbeatInput(value)).success, - ).toBeFalse(); - expect(driverRuntimeRpcSchemas.driver.ready.input.safeParse(readyInput(value)).success).toBe( - false, - ); - expect( - driverRuntimeRpcSchemas.driver.hello.output.safeParse( - helloOutput({ eventBatchMaxSize: value }), - ).success, - ).toBeFalse(); - expect( - driverRuntimeRpcSchemas.driver.claimExternalToolEffect.output.safeParse({ + rpc.claimExternalToolEffect.output.safeParse({ attempt: value, effectId: "effect-1", idempotencyKey: "idempotency-1", @@ -135,10 +137,7 @@ describe("Driver RPC wire v3", () => { [64, true], [65, false], ] as const)("validates wire-safe event batch limit %d", (eventBatchMaxSize, accepted) => { - expect( - driverRuntimeRpcSchemas.driver.hello.output.safeParse(helloOutput({ eventBatchMaxSize })) - .success, - ).toBe(accepted); + expect(rpc.hello.output.safeParse(helloOutput({ eventBatchMaxSize })).success).toBe(accepted); }); test.each([ @@ -151,7 +150,7 @@ describe("Driver RPC wire v3", () => { Number.MAX_SAFE_INTEGER + 1, ] as const)("rejects invalid heartbeat interval %p", (heartbeatIntervalMs) => { expect( - driverRuntimeRpcSchemas.driver.hello.output.safeParse({ + rpc.hello.output.safeParse({ ...helloOutput(), heartbeatIntervalMs, }).success, @@ -162,45 +161,31 @@ describe("Driver RPC wire v3", () => { "rejects %p at every non-negative safe-integer field", (value) => { expect( - driverRuntimeRpcSchemas.driver.hello.output.safeParse( - helloOutput({ commandLeaseMs: value }), - ).success, + rpc.hello.output.safeParse(helloOutput({ commandLeaseMs: value })).success, ).toBeFalse(); expect( - driverRuntimeRpcSchemas.driver.heartbeat.output.safeParse({ + rpc.heartbeat.output.safeParse({ heartbeatCount: value, ok: true, }).success, ).toBeFalse(); - expect(driverRuntimeRpcSchemas.driver.pushLogs.input.safeParse(logBatch(value)).success).toBe( - false, - ); - expect( - driverRuntimeRpcSchemas.driver.pushEvents.output.safeParse(eventBatchOutput(value)).success, - ).toBeFalse(); + expect(rpc.pushLogs.input.safeParse(logBatch(value)).success).toBe(false); + expect(rpc.pushEvents.output.safeParse(eventBatchOutput(value)).success).toBeFalse(); }, ); test("accepts zero at every non-negative safe-integer field", () => { - expect(driverRuntimeRpcSchemas.driver.hello.output.safeParse(helloOutput()).success).toBeTrue(); - expect( - driverRuntimeRpcSchemas.driver.heartbeat.output.safeParse({ heartbeatCount: 0, ok: true }) - .success, - ).toBeTrue(); - expect(driverRuntimeRpcSchemas.driver.pushLogs.input.safeParse(logBatch(0)).success).toBeTrue(); - expect( - driverRuntimeRpcSchemas.driver.pushEvents.output.safeParse(eventBatchOutput(0)).success, - ).toBeTrue(); + expect(rpc.hello.output.safeParse(helloOutput()).success).toBeTrue(); + expect(rpc.heartbeat.output.safeParse({ heartbeatCount: 0, ok: true }).success).toBeTrue(); + expect(rpc.pushLogs.input.safeParse(logBatch(0)).success).toBeTrue(); + expect(rpc.pushEvents.output.safeParse(eventBatchOutput(0)).success).toBeTrue(); }); test.each([ ["event", "events", { event: diagnosticEvent(), eventId: "source-1" }], ["log", "logs", logBatch(0).logs[0]], ] as const)("bounds %s batches at 64 entries", (_label, field, entry) => { - const schema = - field === "events" - ? driverRuntimeRpcSchemas.driver.pushEvents.input - : driverRuntimeRpcSchemas.driver.pushLogs.input; + const schema = field === "events" ? rpc.pushEvents.input : rpc.pushLogs.input; const input = (length: number) => ({ driverInstanceId: "driver-1", [field]: Array.from({ length }, () => entry), @@ -213,38 +198,36 @@ describe("Driver RPC wire v3", () => { test.each([ [ "failure message", - driverRuntimeRpcSchemas.driver.failRun.input, + rpc.failRun.input, { driverInstanceId: "driver-1", error: { code: "failed", details: {}, message: "", retryable: false }, runId: "run-1", }, ], - ["receipt type", driverRuntimeRpcSchemas.driver.pushEvents.output, eventBatchOutput(0, "")], + ["receipt type", rpc.pushEvents.output, eventBatchOutput(0, "")], ] as const)("rejects an empty %s", (_label, schema, input) => { expect(schema.safeParse(input).success).toBeFalse(); }); test("rejects an unknown receipt event type", () => { expect( - driverRuntimeRpcSchemas.driver.pushEvents.output.safeParse( - eventBatchOutput(0, "future.event"), - ).success, + rpc.pushEvents.output.safeParse(eventBatchOutput(0, "future.event")).success, ).toBeFalse(); }); test.each([ - [driverRuntimeRpcSchemas.driver.completeRun.input, { driverInstanceId: "driver-1" }], - [driverRuntimeRpcSchemas.driver.completeRun.input, { driverInstanceId: "driver-1", runId: "" }], + [rpc.completeRun.input, { driverInstanceId: "driver-1" }], + [rpc.completeRun.input, { driverInstanceId: "driver-1", runId: "" }], [ - driverRuntimeRpcSchemas.driver.failRun.input, + rpc.failRun.input, { driverInstanceId: "driver-1", error: { code: "failed", details: {}, message: "failed", retryable: false }, }, ], [ - driverRuntimeRpcSchemas.driver.failRun.input, + rpc.failRun.input, { driverInstanceId: "driver-1", error: { code: "failed", details: {}, message: "failed", retryable: false }, @@ -260,19 +243,13 @@ describe("Driver RPC wire v3", () => { ["empty", { accepted: [{ eventId: "", seq: 0, type: "message.delta" }] }], ["non-string", { accepted: [{ eventId: 1, seq: 0, type: "message.delta" }] }], ] as const)("rejects %s receipt eventId", (_label, input) => { - expect(driverRuntimeRpcSchemas.driver.pushEvents.output.safeParse(input).success).toBeFalse(); + expect(rpc.pushEvents.output.safeParse(input).success).toBeFalse(); }); test("rejects empty handshake identity fields", () => { - expect( - driverRuntimeRpcSchemas.driver.hello.input.safeParse(helloInput({ startedAt: "" })).success, - ).toBeFalse(); - expect( - driverRuntimeRpcSchemas.driver.heartbeat.input.safeParse(heartbeatInput(1, "")).success, - ).toBeFalse(); - expect(driverRuntimeRpcSchemas.driver.hello.output.safeParse(helloOutput({}, "")).success).toBe( - false, - ); + expect(rpc.hello.input.safeParse(helloInput({ startedAt: "" })).success).toBeFalse(); + expect(rpc.heartbeat.input.safeParse(heartbeatInput(1, "")).success).toBeFalse(); + expect(rpc.hello.output.safeParse(helloOutput({}, "")).success).toBe(false); }); test("bounds run and mutually exclusive command terminal payloads", () => { @@ -289,8 +266,8 @@ describe("Driver RPC wire v3", () => { toolName: "tool-1", }), ); - const commandUpdate = driverRuntimeRpcSchemas.driver.commandUpdate.input; - const failRun = driverRuntimeRpcSchemas.driver.failRun.input; + const commandUpdate = rpc.commandUpdate.input; + const failRun = rpc.failRun.input; expect(failRun.safeParse({ driverInstanceId: "driver-1", error, runId: "run-1" }).success).toBe( true, @@ -350,7 +327,7 @@ describe("Driver RPC wire v3", () => { "rejects database-only command status %s on Driver updates", (status) => { expect( - driverRuntimeRpcSchemas.driver.commandUpdate.input.safeParse({ + rpc.commandUpdate.input.safeParse({ commandId: "command-1", driverInstanceId: "driver-1", status, @@ -360,7 +337,7 @@ describe("Driver RPC wire v3", () => { ); test("enforces command update payloads by terminal status", () => { - const schema = driverRuntimeRpcSchemas.driver.commandUpdate.input; + const schema = rpc.commandUpdate.input; const identity = { commandId: "command-1", driverInstanceId: "driver-1" }; const error = { code: "failed", details: {}, message: "failed", retryable: false }; const result = { requestId: "request-1" }; @@ -393,12 +370,10 @@ describe("Driver RPC wire v3", () => { const capability = { id: "text_stream", status: "supported", version: 1 }; expect( - driverRuntimeRpcSchemas.driver.hello.input.safeParse( - helloInput({ capabilities: [capability, capability] }), - ).success, + rpc.hello.input.safeParse(helloInput({ capabilities: [capability, capability] })).success, ).toBeFalse(); expect( - driverRuntimeRpcSchemas.driver.hello.output.safeParse({ + rpc.hello.output.safeParse({ ...helloOutput(), acceptedCapabilities: [capability, capability], }).success, @@ -415,43 +390,34 @@ describe("Driver RPC wire v3", () => { }; const cases = [ [ - driverRuntimeRpcSchemas.driver.observeExternalToolEffect.input, + rpc.observeExternalToolEffect.input, { commandId: "command-1", driverInstanceId: "driver-1" }, ], + [rpc.observeExternalToolEffect.output, { effectId: "effect-1", kind: "intent" }], [ - driverRuntimeRpcSchemas.driver.observeExternalToolEffect.output, - { effectId: "effect-1", kind: "intent" }, - ], - [ - driverRuntimeRpcSchemas.driver.claimExternalToolEffect.input, + rpc.claimExternalToolEffect.input, { claimToken: "00000000-0000-4000-8000-000000000001", commandId: "command-1", driverInstanceId: "driver-1", }, ], - [driverRuntimeRpcSchemas.driver.claimExternalToolEffect.output, claim], + [rpc.claimExternalToolEffect.output, claim], [ - driverRuntimeRpcSchemas.driver.commandUpdate.input, + rpc.commandUpdate.input, { commandId: "command-1", driverInstanceId: "driver-1", status: "accepted" }, ], - [driverRuntimeRpcSchemas.driver.commandUpdate.output, { ok: true }], + [rpc.commandUpdate.output, { ok: true }], + [rpc.completeRun.input, { driverInstanceId: "driver-1", runId: "run-1" }], + [rpc.completeRun.output, { ok: true }], + [rpc.failRun.input, { driverInstanceId: "driver-1", error: failure, runId: "run-1" }], + [rpc.failRun.output, { ok: true }], + [rpc.heartbeat.input, heartbeatInput(1)], + [rpc.heartbeat.output, { heartbeatCount: 0, ok: true }], + [rpc.hello.input, helloInput()], + [rpc.hello.output, helloOutput()], [ - driverRuntimeRpcSchemas.driver.completeRun.input, - { driverInstanceId: "driver-1", runId: "run-1" }, - ], - [driverRuntimeRpcSchemas.driver.completeRun.output, { ok: true }], - [ - driverRuntimeRpcSchemas.driver.failRun.input, - { driverInstanceId: "driver-1", error: failure, runId: "run-1" }, - ], - [driverRuntimeRpcSchemas.driver.failRun.output, { ok: true }], - [driverRuntimeRpcSchemas.driver.heartbeat.input, heartbeatInput(1)], - [driverRuntimeRpcSchemas.driver.heartbeat.output, { heartbeatCount: 0, ok: true }], - [driverRuntimeRpcSchemas.driver.hello.input, helloInput()], - [driverRuntimeRpcSchemas.driver.hello.output, helloOutput()], - [ - driverRuntimeRpcSchemas.driver.settleExternalToolEffect.input, + rpc.settleExternalToolEffect.input, { claimToken: "00000000-0000-4000-8000-000000000001", commandId: "command-1", @@ -460,22 +426,19 @@ describe("Driver RPC wire v3", () => { settlement: { kind: "unknown" }, }, ], + [rpc.settleExternalToolEffect.output, { effectId: "effect-1", kind: "unknown" }], [ - driverRuntimeRpcSchemas.driver.settleExternalToolEffect.output, - { effectId: "effect-1", kind: "unknown" }, - ], - [ - driverRuntimeRpcSchemas.driver.pushEvents.input, + rpc.pushEvents.input, { driverInstanceId: "driver-1", events: [{ event: diagnosticEvent(), eventId: "source-1" }], }, ], - [driverRuntimeRpcSchemas.driver.pushEvents.output, eventBatchOutput(0)], - [driverRuntimeRpcSchemas.driver.pushLogs.input, logBatch(0)], - [driverRuntimeRpcSchemas.driver.pushLogs.output, { ok: true }], - [driverRuntimeRpcSchemas.driver.ready.input, readyInput(1)], - [driverRuntimeRpcSchemas.driver.ready.output, { ok: true }], + [rpc.pushEvents.output, eventBatchOutput(0)], + [rpc.pushLogs.input, logBatch(0)], + [rpc.pushLogs.output, { ok: true }], + [rpc.ready.input, readyInput(1)], + [rpc.ready.output, { ok: true }], [driverRuntimeRpcSchemas.driverInstance.nextCommand.input, { driverInstanceId: "driver-1" }], [driverRuntimeRpcSchemas.driverInstance.nextCommand.output, { command: null }], ] as const; @@ -489,14 +452,14 @@ describe("Driver RPC wire v3", () => { const event = diagnosticEvent({ future: { nested: true }, message: "ok" }); const nestedCases = [ [ - driverRuntimeRpcSchemas.driver.hello.input, + rpc.hello.input, helloInput({ capabilities: [{ future: true, id: "text_stream", status: "supported", version: 1 }], }), ], - [driverRuntimeRpcSchemas.driver.hello.output, helloOutput({ future: true })], + [rpc.hello.output, helloOutput({ future: true })], [ - driverRuntimeRpcSchemas.driver.failRun.input, + rpc.failRun.input, { driverInstanceId: "driver-1", error: { code: "failed", details: {}, future: true, message: "failed", retryable: false }, @@ -504,7 +467,7 @@ describe("Driver RPC wire v3", () => { }, ], [ - driverRuntimeRpcSchemas.driver.commandUpdate.input, + rpc.commandUpdate.input, { commandId: "command-1", driverInstanceId: "driver-1", @@ -513,7 +476,7 @@ describe("Driver RPC wire v3", () => { }, ], [ - driverRuntimeRpcSchemas.driver.settleExternalToolEffect.input, + rpc.settleExternalToolEffect.input, { claimToken: "00000000-0000-4000-8000-000000000001", commandId: "command-1", @@ -523,18 +486,18 @@ describe("Driver RPC wire v3", () => { }, ], [ - driverRuntimeRpcSchemas.driver.pushLogs.input, + rpc.pushLogs.input, { driverInstanceId: "driver-1", logs: [{ ...logBatch(0).logs[0], future: true }] }, ], [ - driverRuntimeRpcSchemas.driver.pushLogs.input, + rpc.pushLogs.input, { driverInstanceId: "driver-1", logs: [{ ...logBatch(0).logs[0], context: { future: true } }], }, ], [ - driverRuntimeRpcSchemas.driver.pushLogs.input, + rpc.pushLogs.input, { driverInstanceId: "driver-1", logs: [ @@ -543,18 +506,18 @@ describe("Driver RPC wire v3", () => { }, ], [ - driverRuntimeRpcSchemas.driver.pushEvents.input, + rpc.pushEvents.input, { driverInstanceId: "driver-1", events: [{ event, eventId: "source-1", future: true }] }, ], [ - driverRuntimeRpcSchemas.driver.pushEvents.input, + rpc.pushEvents.input, { driverInstanceId: "driver-1", events: [{ event: { ...event, future: true }, eventId: "source-1" }], }, ], [ - driverRuntimeRpcSchemas.driver.pushEvents.input, + rpc.pushEvents.input, { driverInstanceId: "driver-1", events: [ @@ -565,10 +528,7 @@ describe("Driver RPC wire v3", () => { ], }, ], - [ - driverRuntimeRpcSchemas.driver.pushEvents.output, - { accepted: [{ ...eventBatchOutput(0).accepted[0], future: true }] }, - ], + [rpc.pushEvents.output, { accepted: [{ ...eventBatchOutput(0).accepted[0], future: true }] }], ] as const; for (const [schema, value] of nestedCases) { @@ -576,7 +536,7 @@ describe("Driver RPC wire v3", () => { } expect( - driverRuntimeRpcSchemas.driver.pushEvents.input.safeParse({ + rpc.pushEvents.input.safeParse({ driverInstanceId: "driver-1", events: [{ event, eventId: "source-1" }], }).success, @@ -585,7 +545,7 @@ describe("Driver RPC wire v3", () => { test("preserves empty log and tracing strings", () => { expect( - driverRuntimeRpcSchemas.driver.pushLogs.input.safeParse({ + rpc.pushLogs.input.safeParse({ driverInstanceId: "driver-1", logs: [ { @@ -603,7 +563,7 @@ describe("Driver RPC wire v3", () => { test("rejects the previous runtime event schema", () => { expect( - driverRuntimeRpcSchemas.driver.pushEvents.input.safeParse({ + rpc.pushEvents.input.safeParse({ driverInstanceId: DRIVER_TEST_IDS.driverInstanceId, events: [ { diff --git a/tests/driver-runtime-boundary-dispatcher.test.ts b/tests/driver-runtime-boundary-dispatcher.test.ts index 6d8147b..b371fde 100644 --- a/tests/driver-runtime-boundary-dispatcher.test.ts +++ b/tests/driver-runtime-boundary-dispatcher.test.ts @@ -1571,7 +1571,7 @@ describe("driver runtime boundary", () => { }); test.each(["input", "mcp"] as const)( - "accepts a slow %s terminal response before settling later work", + "settles a slow %s terminal response without losing the later command", async (kind) => { const backend = createBackend(); let sideEffects = 0; @@ -1648,12 +1648,7 @@ describe("driver runtime boundary", () => { const runtimeState = new DriverRuntimeStateMachine("ready"); const { dispatcher, logger } = createDispatcher({ backend, - isShuttingDown: () => - kind === "input" - ? socket.updates.some( - (update) => update.commandId === next.commandId && update.status === "completed", - ) - : socket.isDrained(), + isShuttingDown: () => socket.isDrained(), mcpExecute: async (command) => { sideEffects += 1; return { @@ -1674,7 +1669,13 @@ describe("driver runtime boundary", () => { expect(outcome.status).toBe("completed"); expect(terminalAttempts).toBe(1); - expect(sideEffects).toBe(kind === "input" ? 2 : 1); + expect(sideEffects).toBe(1); + expect(socket.updates).toContainEqual( + expect.objectContaining({ + commandId: next.commandId, + status: kind === "input" ? "failed" : "completed", + }), + ); }, ); diff --git a/tests/driver-runtime-boundary-fixtures.ts b/tests/driver-runtime-boundary-fixtures.ts index 39227f9..3192773 100644 --- a/tests/driver-runtime-boundary-fixtures.ts +++ b/tests/driver-runtime-boundary-fixtures.ts @@ -56,8 +56,9 @@ export class FakeDriverRuntimeIo implements DriverRuntimeIo { claimRunCancellation( ticket: DriverRunTicket, reason: string, + source?: Parameters[2], ): ReturnType { - return this.#terminalState.claimCancellation(ticket, reason); + return this.#terminalState.claimCancellation(ticket, reason, source); } currentRunId(): ReturnType { @@ -184,6 +185,7 @@ export class FakeDriverRuntimeIo implements DriverRuntimeIo { await pending; } } + input.signal?.throwIfAborted(); this.pushedEvents.push({ ...input, events }); const ticket = this.#activeRunTicket; const terminalEvent = events.find( diff --git a/tests/driver-runtime-boundary-state.test.ts b/tests/driver-runtime-boundary-state.test.ts index dcb7a47..d4dc71c 100644 --- a/tests/driver-runtime-boundary-state.test.ts +++ b/tests/driver-runtime-boundary-state.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { ACTIVE_INPUT_SETTLE_GRACE_MS } from "../src/core/driver-command-dispatcher"; import { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; import { createTimingEvent } from "../src/core/driver-runtime-timing"; import { toDriverEventEnvelopes } from "../src/infrastructure/runtime/driver-instance-socket"; @@ -12,6 +13,7 @@ import { FakeDriverRuntimeIo, createBackend, createDispatcher, + settleBackendInput, waitForUpdate, } from "./driver-runtime-boundary-fixtures"; @@ -455,29 +457,18 @@ describe("driver runtime boundary", () => { }, ); - test("lets a queued input wait for the previous turn command to settle", async () => { + test("rejects overlapping input without blocking a following stop", async () => { const firstInputStarted = Promise.withResolvers(); - const firstInputCanFinish = Promise.withResolvers(); const backend = createBackend(); let handledInputCount = 0; - backend.handleInput = async (context, _input, runId) => { + backend.handleInput = async (context, _input, runId, signal) => { handledInputCount += 1; backend.handledInputs.push(context.payload.execution.session); - - if (handledInputCount === 1) { - firstInputStarted.resolve(); - await firstInputCanFinish.promise; - } - await context.ports.eventSink.pushEvents({ - events: [ - { - kind: "run.completed", - payload: { status: "completed" }, - runId, - sourceEventId: `queued-input.completed:${runId}`, - }, - ], + firstInputStarted.resolve(); + await new Promise((resolve) => { + signal!.addEventListener("abort", () => resolve(), { once: true }); }); + await settleBackendInput(context, runId, signal); }; const runtimeState = new DriverRuntimeStateMachine("ready"); const socket = new FakeDriverRuntimeIo([ @@ -499,66 +490,60 @@ describe("driver runtime boundary", () => { requestId: "request-2", runId: DRIVER_TEST_IDS.secondRunId, }, + { + commandId: "stop-after-overlap", + kind: "session.stop", + reason: "test.stop-after-overlap", + }, ]); const { commandReads, dispatcher, logger } = createDispatcher({ backend, isShuttingDown: () => socket.isDrained(), runtimeState, }); - const runTask = dispatcher.run(socket, logger); + const nativeSetTimeout = globalThis.setTimeout; + let activeInputTimeouts = 0; + globalThis.setTimeout = (( + callback: (...arguments_: unknown[]) => void, + timeout?: number, + ...arguments_: unknown[] + ) => { + if (timeout === ACTIVE_INPUT_SETTLE_GRACE_MS) { + activeInputTimeouts += 1; + } + return nativeSetTimeout(callback, timeout, ...arguments_); + }) as typeof setTimeout; - await firstInputStarted.promise; - await waitForUpdate( - socket, - (update) => update.commandId === "input-1" && update.status === "accepted", - ); - firstInputCanFinish.resolve(); - await runTask; - await waitForUpdate( - socket, - (update) => - update.commandId === "input-2" && - update.status === "completed" && - runtimeState.status() === "ready", - ); + try { + const runTask = dispatcher.run(socket, logger); + await firstInputStarted.promise; + await runTask; + } finally { + globalThis.setTimeout = nativeSetTimeout; + } - expect(handledInputCount).toBe(2); - expect(runtimeState.status()).toBe("ready"); - expect(commandReads.count).toBe(2); + expect(handledInputCount).toBe(1); + expect(activeInputTimeouts).toBe(1); + expect(runtimeState.status()).toBe("stopped"); + expect(commandReads.count).toBe(3); expect(socket.failedRuns).toEqual([]); + expect(backend.cancelledReasons).toEqual(["test.stop-after-overlap"]); expect(socket.updates).toEqual( expect.arrayContaining([ - { - commandId: "input-1", - status: "accepted", - }, - { - commandId: "input-2", - status: "accepted", - }, - { - commandId: "input-1", - result: { - requestId: "request-1", - }, - status: "completed", - }, - { - commandId: "input-2", - result: { - requestId: "request-2", - }, - status: "completed", - }, + { commandId: "input-1", status: "accepted" }, + { commandId: "input-1", status: "cancelled" }, + { commandId: "stop-after-overlap", status: "accepted" }, + { commandId: "stop-after-overlap", status: "completed" }, + expect.objectContaining({ commandId: "input-2", status: "failed" }), ]), ); expect( socket.updates.findIndex( - (update) => update.commandId === "input-1" && update.status === "completed", + (update) => update.commandId === "input-2" && update.status === "failed", ), ).toBeLessThan( socket.updates.findIndex( - (update) => update.commandId === "input-2" && update.status === "completed", + (update) => update.commandId === "stop-after-overlap" && update.status === "accepted", ), ); }); diff --git a/tests/driver-runtime-boundary-timing.test.ts b/tests/driver-runtime-boundary-timing.test.ts index acb2672..0706da9 100644 --- a/tests/driver-runtime-boundary-timing.test.ts +++ b/tests/driver-runtime-boundary-timing.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { ACTIVE_TURN_CANCEL_GRACE_MS } from "../src/core/driver-command-dispatcher"; +import { ACTIVE_INPUT_SETTLE_GRACE_MS } from "../src/core/driver-command-dispatcher"; import { DRIVER_EVENT_DELIVERY_TIMEOUT_MS } from "../src/core/driver-runtime-io"; import { DriverRuntimeStateMachine } from "../src/core/driver-runtime-state"; import type { McpExternalToolExecutionResult, RuntimeCommand } from "../src/runtime-command"; @@ -69,7 +69,7 @@ describe("driver runtime boundary", () => { ? 20 : timeout === DRIVER_EVENT_DELIVERY_TIMEOUT_MS ? 100 - : timeout === ACTIVE_TURN_CANCEL_GRACE_MS + DRIVER_EVENT_DELIVERY_TIMEOUT_MS + : timeout === ACTIVE_INPUT_SETTLE_GRACE_MS ? 200 : timeout, ...arguments_, diff --git a/tests/driver-terminal-state.test.ts b/tests/driver-terminal-state.test.ts index ec3548c..e7282dc 100644 --- a/tests/driver-terminal-state.test.ts +++ b/tests/driver-terminal-state.test.ts @@ -46,8 +46,11 @@ describe("DriverTerminalStateMachine", () => { const ticket = state.beginRun(DRIVER_TEST_IDS.runId); expect(state.claimCancellation(ticket, "cancel first")).toBe("claimed"); - expect(state.claimCancellation(ticket, "again")).toBe("already_claimed"); + expect(ticket.signal.reason).toMatchObject({ resumeAllowed: true }); + expect(state.claimCancellation(ticket, "stop now", "shutdown")).toBe("already_claimed"); expect(ticket.signal.reason).toBeInstanceOf(DriverTurnCancelledError); + expect(ticket.signal.reason).toMatchObject({ resumeAllowed: false }); + expect(ticket.signal.reason.resumeSignal.aborted).toBe(true); expect(state.selectRunTerminal(ticket, terminal("completed"))).toBe("cancelled"); }); diff --git a/tests/openai-app-server-agent-task-events.test.ts b/tests/openai-app-server-agent-task-events.test.ts index 37d6ef5..0e984b0 100644 --- a/tests/openai-app-server-agent-task-events.test.ts +++ b/tests/openai-app-server-agent-task-events.test.ts @@ -19,6 +19,14 @@ function apply(state: OpenAiAgentTaskState, activity: OpenAiSubAgentActivity) { return update.events; } +function activity( + agentId: string, + kind: OpenAiSubAgentActivity["kind"], + agentPath = `/root/${agentId}`, +): OpenAiSubAgentActivity { + return { agentId, agentPath, kind }; +} + function tasks(events: readonly DriverEventInput[]) { const snapshot = events.find((event) => event.kind === "agent.tasks.replaced"); @@ -32,10 +40,6 @@ function tasks(events: readonly DriverEventInput[]) { describe("OpenAI app-server agent task snapshots", () => { test("projects interleaved completion-only activity as full active-set replacements", () => { const state = new OpenAiAgentTaskState(); - const activity = ( - agentId: string, - kind: OpenAiSubAgentActivity["kind"], - ): OpenAiSubAgentActivity => ({ agentId, agentPath: `/root/${agentId}`, kind }); expect(tasks(apply(state, activity("agent-1", "started")))).toEqual([ { taskId: "agent-1", taskType: "openai_subagent", title: "/root/agent-1" }, @@ -64,11 +68,8 @@ describe("OpenAI app-server agent task snapshots", () => { let events: DriverEventInput[] = []; for (let index = 0; index < 256; index += 1) { - events = apply(state, { - agentId: `agent-${String(index)}`, - agentPath: `${privateMarker}-${String(index)}`, - kind: "started", - }); + const agentId = `agent-${String(index)}`; + events = apply(state, activity(agentId, "started", `${privateMarker}-${String(index)}`)); } expect(events).toContainEqual( @@ -87,18 +88,10 @@ describe("OpenAI app-server agent task snapshots", () => { const state = new OpenAiAgentTaskState(); for (let index = 0; index < 256; index += 1) { - apply(state, { - agentId: `agent-${String(index)}`, - agentPath: `/root/agent-${String(index)}`, - kind: "started", - }); + apply(state, activity(`agent-${String(index)}`, "started")); } - const overflow = apply(state, { - agentId: "agent-256", - agentPath: "/root/agent-256", - kind: "started", - }); + const overflow = apply(state, activity("agent-256", "started")); expect(overflow).toEqual([ expect.objectContaining({ kind: "diagnostic.reported", @@ -106,13 +99,7 @@ describe("OpenAI app-server agent task snapshots", () => { }), ]); - const recovered = tasks( - apply(state, { - agentId: "agent-0", - agentPath: "/root/agent-0", - kind: "completed", - }), - ); + const recovered = tasks(apply(state, activity("agent-0", "completed"))); expect(recovered).toHaveLength(256); expect(recovered).not.toContainEqual(expect.objectContaining({ taskId: "agent-0" })); expect(recovered).toContainEqual(expect.objectContaining({ taskId: "agent-256" })); @@ -122,69 +109,37 @@ describe("OpenAI app-server agent task snapshots", () => { const state = new OpenAiAgentTaskState(); for (let index = 0; index < 1_024; index += 1) { - apply(state, { - agentId: `agent-${String(index)}`, - agentPath: `/root/agent-${String(index)}`, - kind: "started", - }); + apply(state, activity(`agent-${String(index)}`, "started")); } - expect(() => - state.prepare({ agentId: "agent-1024", agentPath: "/root/overflow", kind: "started" }), - ).toThrow("exceeds 1024"); + expect(() => state.prepare(activity("agent-1024", "started", "/root/overflow"))).toThrow( + "exceeds 1024", + ); }); test("bounds completed replay protection without letting late starts revive", () => { const state = new OpenAiAgentTaskState(); for (let index = 0; index < 1_024; index += 1) { - apply(state, { - agentId: `closed-${String(index)}`, - agentPath: `/root/closed-${String(index)}`, - kind: "completed", - }); + apply(state, activity(`closed-${String(index)}`, "completed")); } for (let index = 0; index < 1_024; index += 1) { - expect( - tasks( - apply(state, { - agentId: `closed-${String(index)}`, - agentPath: `/root/closed-${String(index)}`, - kind: "started", - }), - ), - ).toEqual([]); + expect(tasks(apply(state, activity(`closed-${String(index)}`, "started")))).toEqual([]); } - expect(() => - state.prepare({ - agentId: "closed-1024", - agentPath: "/root/closed-1024", - kind: "completed", - }), - ).toThrow("closed sub-agent count exceeds 1024"); - expect( - tasks( - apply(state, { - agentId: "closed-0", - agentPath: "/root/closed-0", - kind: "started", - }), - ), - ).toEqual([]); + expect(() => state.prepare(activity("closed-1024", "completed"))).toThrow( + "closed sub-agent count exceeds 1024", + ); + expect(tasks(apply(state, activity("closed-0", "started")))).toEqual([]); }); test("uses deterministic bounded IDs and an authoritative empty terminal snapshot", () => { const state = new OpenAiAgentTaskState(); const longId = "agent".repeat(100); - const first = tasks( - apply(state, { agentId: longId, agentPath: `${"a".repeat(4_095)}😀`, kind: "started" }), - ); + const first = tasks(apply(state, activity(longId, "started", `${"a".repeat(4_095)}😀`))); state.reset(); - const replay = tasks( - apply(state, { agentId: longId, agentPath: "/root/replayed", kind: "started" }), - ); + const replay = tasks(apply(state, activity(longId, "started", "/root/replayed"))); expect(first[0]).toMatchObject({ taskId: expect.stringMatching(/^rid1_[A-Za-z0-9_-]{43}$/) }); expect(replay[0]).toMatchObject({ taskId: (first[0] as { taskId: string }).taskId }); expect((first[0] as { title: string }).title).toHaveLength(4_095); diff --git a/tests/openai-app-server-client.test.ts b/tests/openai-app-server-client.test.ts index 99a2394..dded009 100644 --- a/tests/openai-app-server-client.test.ts +++ b/tests/openai-app-server-client.test.ts @@ -26,7 +26,7 @@ const initializeResult = { codexHome: "/tmp/openai-home", platformFamily: "unix", platformOs: "linux", - userAgent: "test-app-server/0.151.0", + userAgent: "test-app-server/0.152.0", } as const; const initializeResultJson = JSON.stringify(initializeResult); const nativeResumeResultJson = JSON.stringify({ @@ -52,7 +52,7 @@ const nativeResumeResultJson = JSON.stringify({ agentNickname: null, agentRole: null, canAcceptDirectInput: true, - cliVersion: "0.151.0", + cliVersion: "0.152.0", createdAt: 1, cwd: "/workspace", ephemeral: false, diff --git a/tests/openai-app-server-event-bridge-fixture.ts b/tests/openai-app-server-event-bridge-fixture.ts index 19ce551..6525a7a 100644 --- a/tests/openai-app-server-event-bridge-fixture.ts +++ b/tests/openai-app-server-event-bridge-fixture.ts @@ -6,6 +6,7 @@ import { createDisabledLogger } from "../src/observability"; import type { DriverEventInput } from "../src/protocol/events"; import { isDriverId } from "../src/protocol/id"; import { OpenAiAppServerEventBridge } from "../src/runtimes/openai/app-server-event-bridge"; +import { DriverCompletedTerminalSupersededError } from "../src/runtimes/driver-event-publisher"; import { DRIVER_TEST_IDS, driverStartInput as bootPayload } from "./driver-boot-payload-fixture"; interface EventBatch { @@ -44,6 +45,7 @@ export function createOpenAiBridgeHarness( failNativeResumePublish?: boolean; failReasonOnce?: string; failTerminalOnce?: boolean; + holdCompletedTerminalOnce?: boolean; holdFailedTerminalOnce?: boolean; holdReason?: string; } = {}, @@ -51,11 +53,13 @@ export function createOpenAiBridgeHarness( const batches: EventBatch[] = []; const attempts: EventBatch[] = []; const terminalAttempts: Array<{ + cancellationSignal: AbortSignal | null; closures: readonly DriverEventInput[]; terminal: DriverEventInput; }> = []; let failedReason = false; let failedTerminal = false; + let heldCompletedTerminal = false; const heldPush = Promise.withResolvers(); const releasePush = Promise.withResolvers(); const terminalHeld = Promise.withResolvers(); @@ -92,8 +96,25 @@ export function createOpenAiBridgeHarness( const bridge = new OpenAiAppServerEventBridge({ push, pushSession: push, - pushTerminal: async (pushContext, reason, closures, terminal) => { - terminalAttempts.push({ closures, terminal }); + pushTerminal: async (pushContext, reason, closures, terminal, cancellationSignal) => { + terminalAttempts.push({ cancellationSignal: cancellationSignal ?? null, closures, terminal }); + if ( + !heldCompletedTerminal && + options.holdCompletedTerminalOnce === true && + terminal.kind === "run.completed" + ) { + heldCompletedTerminal = true; + for (const closure of closures) { + await push(pushContext, `${reason}.items`, [closure]); + } + terminalHeld.resolve(); + await releaseTerminal.promise; + if (cancellationSignal?.aborted === true) { + throw new DriverCompletedTerminalSupersededError(cancellationSignal.reason); + } + await push(pushContext, reason, [terminal]); + return; + } if (!failedTerminal && options.failTerminalOnce === true) { failedTerminal = true; if (options.holdFailedTerminalOnce === true) { diff --git a/tests/openai-app-server-event-bridge-items.test.ts b/tests/openai-app-server-event-bridge-items.test.ts index 300d3a1..e8d5a01 100644 --- a/tests/openai-app-server-event-bridge-items.test.ts +++ b/tests/openai-app-server-event-bridge-items.test.ts @@ -244,6 +244,28 @@ describe("OpenAi app-server event bridge", () => { await expect(Promise.all([first, duplicate])).resolves.toEqual([undefined, undefined]); }); + test("keeps one exact root-turn admission until its matching response claims it", async () => { + const tracker = new OpenAiTurnTracker(); + const controller = new AbortController(); + const admission = tracker.admitRootTurn(DRIVER_TEST_IDS.runId, controller.signal); + + expect(() => tracker.admitRootTurn(DRIVER_TEST_IDS.secondRunId)).toThrow("already pending"); + tracker.armRootTurn(admission); + tracker.bindRootTurn(admission, "turn-1"); + expect(() => + tracker.claimRootTurn(admission, "turn-mismatch", DRIVER_TEST_IDS.runId, controller.signal), + ).toThrow("was not bound"); + + tracker.releaseRootTurn(admission); + const next = tracker.admitRootTurn(DRIVER_TEST_IDS.secondRunId); + tracker.releaseRootTurn(admission); + tracker.armRootTurn(next); + tracker.bindRootTurn(next, "turn-2"); + const tracked = tracker.claimRootTurn(next, "turn-2", DRIVER_TEST_IDS.secondRunId); + tracker.settle("turn-2", { kind: "completed" }); + await expect(tracked).resolves.toBeUndefined(); + }); + test("closes visible message, tool, and run state when a turn is cancelled", async () => { const { bridge, context, events } = createHarness(); const trackedTurn = bridge.trackTurn("turn-1", DRIVER_TEST_IDS.runId); @@ -1087,6 +1109,36 @@ describe("OpenAi app-server event bridge", () => { expect(JSON.stringify(events())).not.toContain("y\\n"); }); + test.each([ + ["modelProvider/authRecoveryStarted", "Refreshing Amazon Bedrock credentials.", "started"], + ["modelProvider/authRecoveryCompleted", "Amazon Bedrock credentials refreshed.", "completed"], + ] as const)("publishes %s independently", async (method, message, status) => { + const { bridge, context, events } = createHarness(); + + await handleOfficialNotification(bridge, context, method, { + message, + provider: "Amazon Bedrock", + threadId: "thread-1", + turnId: "turn-1", + }); + + const published = events(); + expect(published).toMatchObject([ + { + delivery: "best_effort", + kind: "message.added", + payload: { + content: message, + level: "info", + provider: "Amazon Bedrock", + subtype: `model_provider_auth_recovery_${status}`, + }, + }, + ]); + expect(published[0]?.payload).not.toHaveProperty("threadId"); + expect(published[0]?.payload).not.toHaveProperty("turnId"); + }); + test("publishes model, MCP server, and user-facing warning notifications", async () => { const { bridge, context, events } = createHarness(); diff --git a/tests/openai-app-server-event-bridge-transcript.test.ts b/tests/openai-app-server-event-bridge-transcript.test.ts index ed82181..1ee8a5c 100644 --- a/tests/openai-app-server-event-bridge-transcript.test.ts +++ b/tests/openai-app-server-event-bridge-transcript.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { DriverTurnCancelledError } from "../src/core/driver-runtime-state"; import { DRIVER_TEST_IDS } from "./driver-boot-payload-fixture"; import { createOpenAiBridgeHarness as createHarness, @@ -66,6 +67,58 @@ describe("OpenAi app-server event bridge", () => { expect(events()).toHaveLength(terminalEventCount); }); + test("lets core cancellation replace a pre-response unselected completed turn", async () => { + const { bridge, context, events, releaseTerminal, terminalAttempts, terminalHeld } = + createHarness({ holdCompletedTerminalOnce: true }); + const controller = new AbortController(); + const admission = bridge.beginTurnAdmission(DRIVER_TEST_IDS.runId, controller.signal); + bridge.armTurnAdmission(admission); + const started = bridge.handleNotification(context, "turn/started", { + threadId: "thread-1", + turn: { id: "turn-1", status: "inProgress" }, + }); + bridge.bindTurnAdmission(admission, "turn-1"); + await started; + await bridge.handleNotification(context, "item/agentMessage/delta", { + delta: "done", + itemId: "message-1", + threadId: "thread-1", + turnId: "turn-1", + }); + const completion = bridge.handleNotification(context, "turn/completed", { + threadId: "thread-1", + turn: { id: "turn-1", status: "completed" }, + }); + + await terminalHeld; + controller.abort(new DriverTurnCancelledError("test.cancel")); + releaseTerminal(); + await completion; + const trackedTurn = bridge.claimTurnAdmission( + admission, + "turn-1", + DRIVER_TEST_IDS.runId, + controller.signal, + ); + void trackedTurn.catch(() => {}); + await bridge.cancelTurn(context, "turn-1", "test.cancel"); + + await expect(trackedTurn).rejects.toThrow("test.cancel"); + expect(terminalAttempts.map(({ terminal }) => terminal.kind)).toEqual([ + "run.completed", + "run.cancelled", + ]); + expect(terminalAttempts[0]?.cancellationSignal).toBe(controller.signal); + expect( + events().filter((event) => + ["run.cancel.requested", "run.cancelled", "run.completed"].includes(event.kind), + ), + ).toMatchObject([{ kind: "run.cancel.requested" }, { kind: "run.cancelled" }]); + expect(events().filter((event) => event.kind === "message.completed")).toHaveLength(1); + expect(events().filter((event) => event.kind === "message.cancelled")).toHaveLength(0); + expect(events().filter((event) => event.kind === "agent.tasks.replaced")).toHaveLength(1); + }); + test("does not duplicate streamed reasoning in the completed snapshot", async () => { const { bridge, context, events } = createHarness(); diff --git a/tests/openai-app-server-provider-fixtures.test.ts b/tests/openai-app-server-provider-fixtures.test.ts index ef7c147..22209a4 100644 --- a/tests/openai-app-server-provider-fixtures.test.ts +++ b/tests/openai-app-server-provider-fixtures.test.ts @@ -203,7 +203,7 @@ function createThreadFixture(id = "thread-1") { agentNickname: null, agentRole: null, canAcceptDirectInput: true, - cliVersion: "0.151.0", + cliVersion: "0.152.0", createdAt: 1_700_000_000, cwd: "/workspace", ephemeral: false, @@ -480,7 +480,7 @@ describe("OpenAI app-server provider fixtures", () => { codexHome: "/tmp/openai-home", platformFamily: "unix", platformOs: "linux", - userAgent: "test-app-server/0.151.0", + userAgent: "test-app-server/0.152.0", }; delete response[missing]; @@ -493,7 +493,7 @@ describe("OpenAI app-server provider fixtures", () => { codexHome: "/tmp/openai-home", platformFamily: "unix", platformOs: "linux", - userAgent: "test-app-server/0.151.0", + userAgent: "test-app-server/0.152.0", }; expect(CLIENT_RESULT_SCHEMAS.initialize.parse(response)).toEqual(response); diff --git a/tests/openai-app-server-request-handler.test.ts b/tests/openai-app-server-request-handler.test.ts index f407273..8ff61b4 100644 --- a/tests/openai-app-server-request-handler.test.ts +++ b/tests/openai-app-server-request-handler.test.ts @@ -5,6 +5,7 @@ import { PermissionEventDeliveryError } from "../src/core/driver-permission-brok import type { DriverPermissionRequest } from "../src/host-ports"; import { createDisabledLogger } from "../src/observability"; import { OpenAiAppServerRequestHandler } from "../src/runtimes/openai/app-server-request-handler"; +import { parseServerRequest } from "../src/runtimes/openai/app-server-protocol"; import { driverStartInput } from "./driver-boot-payload-fixture"; test("OpenAI server request callbacks handle, reject, and cancel explicitly", async () => { @@ -59,6 +60,26 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as handler.dispatch("attestation/generate", 2, {}); handler.dispatch("applyPatchApproval", 6, {}); handler.dispatch("execCommandApproval", 7, {}); + const openAiFormRequest = parseServerRequest({ + id: 8, + method: "mcpServer/elicitation/request", + params: { + _meta: { "example/request": "template-picker" }, + message: "Choose a template", + mode: "openaiForm", + requestedSchema: { + properties: { template: { type: "string", "x-openai-preview": {} } }, + type: "object", + }, + serverName: "codex_apps", + threadId: "thread-1", + turnId: null, + }, + }); + if (openAiFormRequest === null) { + throw new Error("Codex 0.152 openaiForm elicitation did not match the generated schema."); + } + handler.dispatch(openAiFormRequest.method, openAiFormRequest.id, openAiFormRequest.params); handler.dispatch("item/commandExecution/requestApproval", 3, { additionalPermissions: { fileSystem: { write: ["/secrets"] }, @@ -113,6 +134,10 @@ test("OpenAI server request callbacks handle, reject, and cancel explicitly", as { id: 2, message: "Unsupported OpenAi app-server request: attestation/generate." }, { id: 6, message: "Unsupported OpenAi app-server request: applyPatchApproval." }, { id: 7, message: "Unsupported OpenAi app-server request: execCommandApproval." }, + { + id: 8, + message: "Unsupported OpenAi app-server request: mcpServer/elicitation/request.", + }, ]); expect(permissionSignals.filter((signal) => signal.aborted)).toHaveLength(1); expect(permissionInputs.map(({ rawInput }) => JSON.parse(rawInput ?? "null"))).toEqual([ diff --git a/tests/openai-app-server-startup.test.ts b/tests/openai-app-server-startup.test.ts index c7a02be..c19471c 100644 --- a/tests/openai-app-server-startup.test.ts +++ b/tests/openai-app-server-startup.test.ts @@ -27,7 +27,7 @@ const initializeResultJson = JSON.stringify({ codexHome: "/tmp/openai-home", platformFamily: "unix", platformOs: "linux", - userAgent: "test-app-server/0.151.0", + userAgent: "test-app-server/0.152.0", }); function eventPayloadStatus(event: DriverEventInput): unknown { @@ -42,6 +42,7 @@ interface CancellationHarnessOptions { readonly backgroundTerminalCleanFailure?: "error" | "timeout"; readonly duplicateRequestPhase?: "active_turn" | "turn_start"; readonly emitInterruptedTurnOnStart?: boolean; + readonly terminalNotificationBeforeTurnStartResponse?: boolean; readonly emitToolCompletionOnTurnStart?: boolean; readonly environmentVariables?: Readonly>; readonly failCancellationRequest?: boolean; @@ -137,6 +138,22 @@ const sendInterrupted = (turnId) => process.stdout.write(JSON.stringify({ }, }, }) + "\\n"); +const sendTurnStarted = (turnId) => process.stdout.write(JSON.stringify({ + method: "turn/started", + params: { + threadId: "fresh-thread", + turn: { + completedAt: null, + durationMs: null, + error: null, + id: turnId, + items: [], + itemsView: "notLoaded", + startedAt: Date.now(), + status: "inProgress", + }, + }, +}) + "\\n"); const toolItem = { aggregatedOutput: "done", command: "printf done", @@ -225,7 +242,7 @@ const thread = { status: { type: "idle" }, path: null, cwd: ${JSON.stringify(directory)}, - cliVersion: "0.151.0", + cliVersion: "0.152.0", source: "appServer", canAcceptDirectInput: true, threadSource: null, @@ -284,6 +301,9 @@ process.stdin.on("data", (chunk) => { ["terminal_response", "terminal_then_malformed"].includes( toolStartFollowup, ) || + (request.method === "turn/start" && + turnNumber === 1 && + ${JSON.stringify(cancellationOptions.terminalNotificationBeforeTurnStartResponse ?? false)}) || ((launchNumber > 1 || turnNumber > 1) && !${JSON.stringify(cancellationOptions.emitToolCompletionOnTurnStart ?? false)}); const holdTurnStart = @@ -342,6 +362,14 @@ process.stdin.on("data", (chunk) => { sendDuplicateRequest(turnId); return; } + if ( + request.method === "turn/start" && + turnNumber === 1 && + ${JSON.stringify(cancellationOptions.terminalNotificationBeforeTurnStartResponse ?? false)} + ) { + sendTurnStarted(turnId); + sendToolLifecycle(turnId, true); + } if (request.method === "turn/start" && response !== null && toolStartFollowup === "malformed") { process.stdout.write(JSON.stringify(response) + "\\n" + toolStartMessage(turnId) + "not-json\\n"); } else if ( @@ -560,10 +588,10 @@ process.stdin.on("data", (chunk) => { const trackedBackend: AgentDriverBackend = { runtime: backend.runtime, cancelActiveTurn: (backendContext, reason) => backend.cancelActiveTurn(backendContext, reason), - handleInput: async (backendContext, input, runId) => { + handleInput: async (backendContext, input, runId, signal) => { activeRunId = runId; try { - await backend.handleInput(backendContext, input, runId); + await backend.handleInput(backendContext, input, runId, signal); } finally { if (activeRunId === runId) { activeRunId = null; @@ -1114,7 +1142,7 @@ describe("OpenAI app-server startup", () => { }, ); - test("closes items emitted before a pending turn start response is cancelled", async () => { + test("does not project items before their pending turn response identifies the run", async () => { const harness = await createCancellationHarness({ emitToolCompletionOnTurnStart: true, holdTurnStartAfterItems: true, @@ -1131,18 +1159,21 @@ describe("OpenAI app-server startup", () => { await expect( settlePromiseWithTimeout( (async () => { - while ( - !harness.events.some( - (event) => - event.kind === "tool.call.updated" && eventPayloadStatus(event) === "running", - ) - ) { + while (!(await Bun.file(harness.turnStartHeldMarker).exists())) { await Bun.sleep(1); } })(), - { label: "pre-response tool start", timeoutMs: 250 }, + { label: "held turn start", timeoutMs: 250 }, ), ).resolves.toMatchObject({ status: "completed" }); + await Bun.sleep(10); + expect( + harness.events.filter((event) => + ["item.started", "message.started", "run.started", "tool.call.updated"].includes( + event.kind, + ), + ), + ).toEqual([]); await harness.backend.cancelActiveTurn(harness.context, "test.cancel"); await expect(input).rejects.toThrow("test.cancel"); @@ -1151,15 +1182,9 @@ describe("OpenAI app-server startup", () => { .filter((event) => event.runId === DRIVER_TEST_IDS.runId) .map((event) => [event.kind, eventPayloadStatus(event)]), ).toEqual([ - ["message.started", undefined], - ["item.started", undefined], - ["tool.call.updated", "running"], ["agent.tasks.replaced", undefined], ["run.started", undefined], ["run.cancel.requested", undefined], - ["message.cancelled", undefined], - ["tool.call.updated", "cancelled"], - ["item.completed", "cancelled"], ["run.cancelled", undefined], ]); } finally { @@ -1189,8 +1214,11 @@ describe("OpenAI app-server startup", () => { ] as const)( "closes a durably started tool before a same-burst %s", async (_label, toolStartFollowup, closureStatus, terminalKind) => { + const responseIdentifiesTurn = + toolStartFollowup === "terminal_response" || + toolStartFollowup === "terminal_then_malformed"; const harness = await createCancellationHarness({ - holdMessageStart: true, + holdMessageStart: responseIdentifiesTurn || toolStartFollowup === "malformed", ...(toolStartFollowup === "error_response" || toolStartFollowup === "error_then_malformed" ? { turnStartErrorMessage: "turn start rejected" } : {}), @@ -1205,14 +1233,15 @@ describe("OpenAI app-server startup", () => { DRIVER_TEST_IDS.runId, ); void input.catch(() => {}); - await harness.messageStartEntered; - expect( - harness.events.some((event) => - ["run.cancelled", "run.completed", "run.failed"].includes(event.kind), - ), - ).toBe(false); - - harness.releaseMessageStart(); + if (responseIdentifiesTurn || toolStartFollowup === "malformed") { + await harness.messageStartEntered; + expect( + harness.events.some((event) => + ["run.cancelled", "run.completed", "run.failed"].includes(event.kind), + ), + ).toBe(false); + harness.releaseMessageStart(); + } if ( toolStartFollowup !== "terminal_response" && toolStartFollowup !== "terminal_then_malformed" @@ -1224,22 +1253,25 @@ describe("OpenAI app-server startup", () => { await expect(input).resolves.toBeUndefined(); } - expect( - harness.events - .filter((event) => - [ - "item.completed", - "item.started", - "message.completed", - "message.failed", - "message.started", - "run.completed", - "run.failed", - "tool.call.updated", - ].includes(event.kind), - ) - .map((event) => [event.kind, eventPayloadStatus(event)]), - ).toEqual([ + const projectedLifecycle = harness.events + .filter((event) => + [ + "item.completed", + "item.started", + "message.completed", + "message.failed", + "message.started", + "run.completed", + "run.failed", + "tool.call.updated", + ].includes(event.kind), + ) + .map((event) => [event.kind, eventPayloadStatus(event)]); + if (!responseIdentifiesTurn && toolStartFollowup !== "malformed") { + expect(projectedLifecycle).toEqual([["run.failed", undefined]]); + return; + } + expect(projectedLifecycle).toEqual([ ["message.started", undefined], ["item.started", undefined], ["tool.call.updated", "running"], @@ -1264,7 +1296,7 @@ describe("OpenAI app-server startup", () => { 10_000, ); - test("does not carry failed turn-start item state into the next turn", async () => { + test("does not project an unadmitted failed-turn item into the next turn", async () => { const harness = await createCancellationHarness({ toolStartFollowup: "error_then_success", turnStartErrorMessage: "turn start rejected", @@ -1285,12 +1317,12 @@ describe("OpenAI app-server startup", () => { expect( harness.events.filter((event) => event.kind === "item.completed").map(eventPayloadStatus), - ).toEqual(["failed"]); + ).toEqual([]); expect( harness.events .filter((event) => event.kind === "tool.call.updated") .map(eventPayloadStatus), - ).toEqual(["running", "failed"]); + ).toEqual([]); expect( harness.events .filter((event) => ["run.completed", "run.failed"].includes(event.kind)) @@ -1465,6 +1497,87 @@ describe("OpenAI app-server startup", () => { } }); + test("dispatcher cancellation replaces an unselected completed terminal", async () => { + const harness = await createCancellationHarness({ + terminalNotificationBeforeTurnStartResponse: true, + }); + const kernel = new AgentDriverKernelCore({ + backendFactory: (payload) => new OpenAiAppServerDriverBackend(payload), + hostPorts: { skill: { materialize: async () => [] } }, + logger: harness.logger, + }); + const terminalEntered = Promise.withResolvers(); + const releaseTerminal = Promise.withResolvers(); + const cancellationClaimed = + Promise.withResolvers>(); + const registerRunTerminalBarrier = kernel.registerRunTerminalBarrier.bind(kernel); + kernel.registerRunTerminalBarrier = (barrier) => + registerRunTerminalBarrier((events) => { + const pending = barrier(events); + if (!events.some(({ kind }) => kind === "run.completed")) { + return pending; + } + return Promise.resolve(pending).then(async () => { + terminalEntered.resolve(); + await releaseTerminal.promise; + }); + }); + const claimRunCancellation = kernel.claimRunCancellation.bind(kernel); + kernel.claimRunCancellation = (ticket, reason) => { + const result = claimRunCancellation(ticket, reason); + cancellationClaimed.resolve(result); + return result; + }; + const events: DriverEventInput[] = []; + const cancelled = (async () => { + for await (const event of kernel.events()) { + events.push(event); + if (event.kind === "run.cancelled") { + return; + } + } + })(); + + try { + await kernel.start(harness.payload); + const input = kernel.dispatch({ + commandId: "completed-terminal-race-input", + input: { text: "finish" }, + kind: "input.start", + requestId: "completed-terminal-race-request", + runId: DRIVER_TEST_IDS.runId, + }); + await terminalEntered.promise; + const cancellation = kernel.cancel("test.cancel"); + expect(await cancellationClaimed.promise).toBe("claimed"); + releaseTerminal.resolve(); + await Promise.all([input, cancellation, cancelled]); + + expect( + events + .filter(({ kind }) => ["run.cancelled", "run.completed", "run.failed"].includes(kind)) + .map(({ kind }) => kind), + ).toEqual(["run.cancelled"]); + expect(events.filter(({ kind }) => kind === "run.cancel.requested")).toHaveLength(1); + expect( + events + .filter( + ({ kind, payload }) => + kind === "tool.call.updated" && + typeof payload === "object" && + payload !== null && + "status" in payload && + (payload.status === "completed" || payload.status === "cancelled"), + ) + .map(({ payload }) => (payload as { status: string }).status), + ).toEqual(["completed"]); + expect(events.filter(({ kind }) => kind === "agent.tasks.replaced")).toHaveLength(1); + } finally { + releaseTerminal.resolve(); + await kernel.stop("test complete").catch(() => {}); + } + }); + test("cleans the client before a queued provider cancellation terminal is delivered", async () => { const harness = await createCancellationHarness({ emitInterruptedTurnOnStart: true, @@ -1680,6 +1793,7 @@ describe("OpenAI app-server startup", () => { const harness = await createCancellationHarness({ holdCancellationRequest: true, }); + let stopped = false; try { await harness.backend.start(harness.context, new AbortController().signal); @@ -1702,6 +1816,21 @@ describe("OpenAI app-server startup", () => { const cancellation = harness.backend.cancelActiveTurn(harness.context, "test.cancel"); void cancellation.catch(() => {}); + const stop = harness.backend.stop( + harness.context, + "test complete", + new AbortController().signal, + ); + void stop.catch(() => {}); + let stopSettled = false; + void stop.then( + () => { + stopSettled = true; + }, + () => { + stopSettled = true; + }, + ); await harness.cancellationRequestEntered; const firstPid = await readFirstLaunchPid(harness.processLog); expect(() => process.kill(firstPid, 0)).toThrow(); @@ -1712,8 +1841,13 @@ describe("OpenAI app-server startup", () => { }), ).resolves.toMatchObject({ status: "completed" }); + await Bun.sleep(10); + expect(stopSettled).toBe(false); + harness.releaseCancellationRequest(); await expect(input).rejects.toThrow("test.cancel"); + await stop; + stopped = true; await expect( settlePromiseWithTimeout( (async () => { @@ -1726,7 +1860,11 @@ describe("OpenAI app-server startup", () => { ).resolves.toMatchObject({ status: "completed" }); } finally { harness.releaseCancellationRequest(); - await harness.backend.stop(harness.context, "test complete", new AbortController().signal); + if (!stopped) { + await harness.backend + .stop(harness.context, "test complete", new AbortController().signal) + .catch(() => {}); + } } }); @@ -1947,11 +2085,7 @@ describe("OpenAI app-server startup", () => { terminalGate.resolve(); } await expect(input).rejects.toThrow("test cancellation"); - if (failureMode === "late terminal delivery") { - await expect(cancellation).resolves.toBeUndefined(); - } else { - await expect(cancellation).rejects.toThrow("test cancellation"); - } + await expect(cancellation).rejects.toThrow("test cancellation"); await expect( settlePromiseWithTimeout( (async () => { diff --git a/tests/public-api-consumer.ts b/tests/public-api-consumer.ts index 97c0cec..eb6f3dc 100644 --- a/tests/public-api-consumer.ts +++ b/tests/public-api-consumer.ts @@ -1,32 +1,16 @@ -import type { +export type { AgentDriverBackend, AgentDriverKernel, CmaStore, DriverStartInput, } from "@mosoo/agent-driver"; -import type { DriverBootPayload } from "@mosoo/agent-driver/boot"; -import type { SessionSnapshot } from "@mosoo/agent-driver/contract"; -import type { CmaHttpHandler } from "@mosoo/agent-driver/cma-http"; -import type { CmaSdkClient } from "@mosoo/agent-driver/cma-sdk"; -import type { DriverEventInput } from "@mosoo/agent-driver/events"; -import type { DriverHeartbeatInput } from "@mosoo/agent-driver/orpc"; -import type { OpenAiPrivateCitationFilterResult } from "@mosoo/agent-driver/provider-output"; -import type { DriverRuntime } from "@mosoo/agent-driver/runtime"; +export type { DriverBootPayload } from "@mosoo/agent-driver/boot"; +export type { SessionSnapshot } from "@mosoo/agent-driver/contract"; +export type { CmaHttpHandler } from "@mosoo/agent-driver/cma-http"; +export type { CmaSdkClient } from "@mosoo/agent-driver/cma-sdk"; +export type { DriverEventInput } from "@mosoo/agent-driver/events"; +export type { DriverHeartbeatInput } from "@mosoo/agent-driver/orpc"; +export type { OpenAiPrivateCitationFilterResult } from "@mosoo/agent-driver/provider-output"; +export type { DriverRuntime } from "@mosoo/agent-driver/runtime"; -type SandboxMemoryPath = typeof import("@mosoo/agent-driver/paths").SANDBOX_MEMORY_PATH; - -export interface PublicApiConsumer { - readonly backend: AgentDriverBackend; - readonly bootPayload: DriverBootPayload; - readonly cmaClient: CmaSdkClient; - readonly cmaHandler: CmaHttpHandler; - readonly cmaStore: CmaStore; - readonly event: DriverEventInput; - readonly heartbeat: DriverHeartbeatInput; - readonly kernel: AgentDriverKernel; - readonly memoryPath: SandboxMemoryPath; - readonly providerOutput: OpenAiPrivateCitationFilterResult; - readonly runtime: DriverRuntime; - readonly snapshot: SessionSnapshot; - readonly startInput: DriverStartInput; -} +export type SandboxMemoryPath = typeof import("@mosoo/agent-driver/paths").SANDBOX_MEMORY_PATH; diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index 699befd..1fc559e 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -1,69 +1,33 @@ import { describe, expect, test } from "bun:test"; -import { DRIVER_PROTOCOL_VERSION as DRIVER_PROTOCOL_VERSION_FROM_BOOT_SUBPATH } from "@mosoo/agent-driver/boot"; -import { createCmaHttpHandler as createCmaHttpHandlerFromSubpath } from "@mosoo/agent-driver/cma-http"; -import { CmaSdkClient as CmaSdkClientFromSubpath } from "@mosoo/agent-driver/cma-sdk"; -import { - PROTOCOL_VERSION as PROTOCOL_VERSION_FROM_CONTRACT, - protocolVersionSchema as protocolVersionSchemaFromContract, - sessionSnapshotSchema as sessionSnapshotSchemaFromContract, -} from "@mosoo/agent-driver/contract"; -import { - RUNTIME_EVENT_KINDS as RUNTIME_EVENT_KINDS_FROM_SUBPATH, - RUNTIME_EVENT_SCHEMA_VERSION as RUNTIME_EVENT_SCHEMA_VERSION_FROM_SUBPATH, - parseDriverEventEnvelope as parseDriverEventEnvelopeFromSubpath, - toRuntimeEventInput as toRuntimeEventInputFromSubpath, -} from "@mosoo/agent-driver/events"; -import { - AGENT_DRIVER_PROVIDER_REGISTRY, - AgentDriverKernelCore, - CMA_DEFAULT_BETA_HEADER_VALUE, - CmaSdkClient, - CmaSdkError, - SUPPORTED_DRIVER_RUNTIMES, - createAgentDriverContext, - createDriverDiagnosticEvent, - createAgentDriverProviderCapabilities, - createCmaHttpHandler, - createCmaMemoryStore, - parseDriverNativeRuntimeRef, - pushDriverDiagnosticEvent, - projectCmaInboundToDriverCommand, - projectDriverEventToCma, -} from "@mosoo/agent-driver"; -import { - parseDriverHeartbeatInput as parseDriverHeartbeatInputFromOrpcSubpath, - parseDriverHelloInput as parseDriverHelloInputFromOrpcSubpath, - parseDriverReadyInput as parseDriverReadyInputFromOrpcSubpath, -} from "@mosoo/agent-driver/orpc"; -import type { DriverHeartbeatInput as DriverHeartbeatInputFromOrpcSubpath } from "@mosoo/agent-driver/orpc"; -import { - SANDBOX_MEMORY_PATH as SANDBOX_MEMORY_PATH_FROM_PATHS_SUBPATH, - getSessionResourceBackingPath as getSessionResourceBackingPathFromSubpath, - getSessionResourceRootPath as getSessionResourceRootPathFromSubpath, - isSandboxSessionResourceBackingPath, - normalizeSandboxFileBrowserPath, -} from "@mosoo/agent-driver/paths"; -import { filterOpenAiPrivateCitations } from "@mosoo/agent-driver/provider-output"; -import { isSupportedDriverRuntime as isSupportedDriverRuntimeFromSubpath } from "@mosoo/agent-driver/runtime"; +import * as agentDriver from "@mosoo/agent-driver"; +import * as boot from "@mosoo/agent-driver/boot"; +import * as cmaHttp from "@mosoo/agent-driver/cma-http"; +import * as cmaSdk from "@mosoo/agent-driver/cma-sdk"; +import * as contract from "@mosoo/agent-driver/contract"; +import * as events from "@mosoo/agent-driver/events"; +import * as orpc from "@mosoo/agent-driver/orpc"; +import * as paths from "@mosoo/agent-driver/paths"; +import * as providerOutput from "@mosoo/agent-driver/provider-output"; +import * as runtime from "@mosoo/agent-driver/runtime"; describe("public API", () => { test("imports without starting the driver process", () => { - expect(AgentDriverKernelCore).toBeFunction(); - expect(createAgentDriverContext).toBeFunction(); - expect(createDriverDiagnosticEvent).toBeFunction(); - expect(createAgentDriverProviderCapabilities).toBeFunction(); - expect(createCmaHttpHandler).toBeFunction(); - expect(createCmaMemoryStore).toBeFunction(); - expect(CmaSdkClient).toBeFunction(); - expect(CmaSdkError).toBeFunction(); - expect(CMA_DEFAULT_BETA_HEADER_VALUE).toBe("managed-agents-2026-04-01"); - expect(projectCmaInboundToDriverCommand).toBeFunction(); - expect(projectDriverEventToCma).toBeFunction(); - expect(pushDriverDiagnosticEvent).toBeFunction(); - expect(parseDriverNativeRuntimeRef).toBeFunction(); - expect(AGENT_DRIVER_PROVIDER_REGISTRY.list()).toHaveLength(3); - expect(SUPPORTED_DRIVER_RUNTIMES).toEqual([ + expect(agentDriver.AgentDriverKernelCore).toBeFunction(); + expect(agentDriver.createAgentDriverContext).toBeFunction(); + expect(agentDriver.createDriverDiagnosticEvent).toBeFunction(); + expect(agentDriver.createAgentDriverProviderCapabilities).toBeFunction(); + expect(agentDriver.createCmaHttpHandler).toBeFunction(); + expect(agentDriver.createCmaMemoryStore).toBeFunction(); + expect(agentDriver.CmaSdkClient).toBeFunction(); + expect(agentDriver.CmaSdkError).toBeFunction(); + expect(agentDriver.CMA_DEFAULT_BETA_HEADER_VALUE).toBe("managed-agents-2026-04-01"); + expect(agentDriver.projectCmaInboundToDriverCommand).toBeFunction(); + expect(agentDriver.projectDriverEventToCma).toBeFunction(); + expect(agentDriver.pushDriverDiagnosticEvent).toBeFunction(); + expect(agentDriver.parseDriverNativeRuntimeRef).toBeFunction(); + expect(agentDriver.AGENT_DRIVER_PROVIDER_REGISTRY.list()).toHaveLength(3); + expect(agentDriver.SUPPORTED_DRIVER_RUNTIMES).toEqual([ "openai-runtime", "claude-agent-sdk", "acp-fallback", @@ -71,32 +35,29 @@ describe("public API", () => { }); test("imports public subpath entries without process side effects", () => { - const heartbeatReason = "ping" satisfies DriverHeartbeatInputFromOrpcSubpath["reason"]; + const heartbeatReason = "ping" satisfies orpc.DriverHeartbeatInput["reason"]; - expect(DRIVER_PROTOCOL_VERSION_FROM_BOOT_SUBPATH).toBe(3); - expect(PROTOCOL_VERSION_FROM_CONTRACT).toBe(2); - expect(protocolVersionSchemaFromContract.parse(2)).toBe(2); - expect(sessionSnapshotSchemaFromContract.parse).toBeFunction(); - expect(createCmaHttpHandlerFromSubpath).toBe(createCmaHttpHandler); - expect(CmaSdkClientFromSubpath).toBe(CmaSdkClient); - expect(parseDriverEventEnvelopeFromSubpath).toBeFunction(); - expect(toRuntimeEventInputFromSubpath).toBeFunction(); - expect(RUNTIME_EVENT_SCHEMA_VERSION_FROM_SUBPATH).toBe("2026-08-29"); - expect(RUNTIME_EVENT_KINDS_FROM_SUBPATH).toContain("agent.tasks.replaced"); - expect(heartbeatReason).toBe("ping"); - expect(parseDriverHeartbeatInputFromOrpcSubpath({ at: "now", pid: 1, reason: "ping" })).toEqual( - { - at: "now", - pid: 1, - reason: "ping", - }, - ); + expect(boot.DRIVER_PROTOCOL_VERSION).toBe(3); + expect(contract.PROTOCOL_VERSION).toBe(2); + expect(contract.protocolVersionSchema.parse(2)).toBe(2); + expect(contract.sessionSnapshotSchema.parse).toBeFunction(); + expect(cmaHttp.createCmaHttpHandler).toBe(agentDriver.createCmaHttpHandler); + expect(cmaSdk.CmaSdkClient).toBe(agentDriver.CmaSdkClient); + expect(events.parseDriverEventEnvelope).toBeFunction(); + expect(events.toRuntimeEventInput).toBeFunction(); + expect(events.RUNTIME_EVENT_SCHEMA_VERSION).toBe("2026-08-29"); + expect(events.RUNTIME_EVENT_KINDS).toContain("agent.tasks.replaced"); + expect(orpc.parseDriverHeartbeatInput({ at: "now", pid: 1, reason: heartbeatReason })).toEqual({ + at: "now", + pid: 1, + reason: "ping", + }); expect( - parseDriverHelloInputFromOrpcSubpath({ + orpc.parseDriverHelloInput({ capabilities: [], driverVersion: "0.1.0", pid: 1, - protocolVersion: DRIVER_PROTOCOL_VERSION_FROM_BOOT_SUBPATH, + protocolVersion: boot.DRIVER_PROTOCOL_VERSION, runtime: "openai-runtime", startedAt: "now", }), @@ -105,7 +66,7 @@ describe("public API", () => { runtime: "openai-runtime", }); expect( - parseDriverReadyInputFromOrpcSubpath({ + orpc.parseDriverReadyInput({ at: "now", driverInstanceId: "driver-1", pid: 1, @@ -113,36 +74,38 @@ describe("public API", () => { ).toMatchObject({ driverInstanceId: "driver-1", }); - expect(isSupportedDriverRuntimeFromSubpath("openai-runtime")).toBe(true); - expect(SANDBOX_MEMORY_PATH_FROM_PATHS_SUBPATH).toBe("/workspace/memory"); - expect(getSessionResourceRootPathFromSubpath("session-1")).toBe( + expect(runtime.isSupportedDriverRuntime("openai-runtime")).toBe(true); + expect(paths.SANDBOX_MEMORY_PATH).toBe("/workspace/memory"); + expect(paths.getSessionResourceRootPath("session-1")).toBe( "/workspace/se/session-1/session-files", ); - expect(getSessionResourceBackingPathFromSubpath("session-1")).toBe( + expect(paths.getSessionResourceBackingPath("session-1")).toBe( "/workspace/.mosoo/session-files/session-1", ); expect( - isSandboxSessionResourceBackingPath("/workspace/.mosoo/session-files/session-1/nested.txt"), + paths.isSandboxSessionResourceBackingPath( + "/workspace/.mosoo/session-files/session-1/nested.txt", + ), ).toBe(true); expect( - isSandboxSessionResourceBackingPath( + paths.isSandboxSessionResourceBackingPath( "/workspace/.mosoo/session-files-other/session-1/nested.txt", ), ).toBe(false); expect( - isSandboxSessionResourceBackingPath( + paths.isSandboxSessionResourceBackingPath( "/workspace/se/session-1/public/.mosoo-session-files-session-1", ), ).toBe(false); expect(() => - normalizeSandboxFileBrowserPath("/workspace/.mosoo/session-files/session-1/nested.txt"), + paths.normalizeSandboxFileBrowserPath("/workspace/.mosoo/session-files/session-1/nested.txt"), ).toThrow("Session resource backing is not visible"); expect( - normalizeSandboxFileBrowserPath( + paths.normalizeSandboxFileBrowserPath( "/workspace/se/session-1/.mosoo-session-files-session-2/nested.txt", ), ).toBe("/workspace/se/session-1/.mosoo-session-files-session-2/nested.txt"); - expect(filterOpenAiPrivateCitations("plain text")).toEqual({ + expect(providerOutput.filterOpenAiPrivateCitations("plain text")).toEqual({ privateCitationCount: 0, text: "plain text", }); From 3439bed72ad18ee6c295668117f88a66219ba45f Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Tue, 1 Sep 2026 21:04:01 +0000 Subject: [PATCH 20/20] fix(runtime)!: complete latest SDK adaptation BREAKING CHANGE: contract consumers must use protocol version 3. --- Containerfile | 2 +- bun.lock | 24 +-- package.json | 4 +- src/contract/common.ts | 2 +- src/core/driver-command-dispatcher.ts | 11 +- src/core/driver-terminal-state.ts | 7 + .../runtime/driver-instance-socket.ts | 4 + .../claude/agent-sdk-driver-backend.ts | 56 +++++-- src/runtimes/claude/agent-sdk-event-writer.ts | 4 +- .../claude/agent-sdk-message-events.ts | 6 +- .../claude/agent-sdk-message-translator.ts | 128 ++++++++++++--- tests/claude-agent-sdk-driver-backend.test.ts | 154 ++++++++++++++++-- ...t-sdk-provider-fixtures-transcript.test.ts | 49 ++++++ tests/contract-executor-mutation.test.ts | 6 +- tests/contract-invariant.test.ts | 2 +- tests/contract-preview.test.ts | 8 +- tests/contract-reducer.test.ts | 6 +- tests/contract-wire.test.ts | 2 +- tests/driver-external-tool-effect-v3.test.ts | 76 ++++++++- tests/driver-instance-socket-process.test.ts | 31 +++- tests/driver-terminal-state.test.ts | 11 ++ .../stream-text-thinking-tool-result.json | 3 +- tests/public-api.test.ts | 5 +- 23 files changed, 518 insertions(+), 83 deletions(-) diff --git a/Containerfile b/Containerfile index 6ee9c02..adfbbc8 100644 --- a/Containerfile +++ b/Containerfile @@ -4,7 +4,7 @@ FROM docker.io/oven/bun:${BUN_VERSION}@sha256:5ff609364c049b54eb0ff560ec96319729 FROM docker.io/cloudflare/sandbox:0.12.9@sha256:4a56a37a3cfd9b38d65bb4b5d0b341e6490a3a4c0226274ae4c1cca4948e85fe # Keep this pin in sync with downstream mosoo apps/api/package.json -> @cloudflare/sandbox. -ARG CLAUDE_AGENT_SDK_VERSION=0.3.252 +ARG CLAUDE_AGENT_SDK_VERSION=0.3.257 ARG BUN_VERSION ARG OPENAI_RUNTIME_VERSION=0.152.0 ARG OPENCODE_VERSION=1.18.25 diff --git a/bun.lock b/bun.lock index 1b0b043..d646d9d 100644 --- a/bun.lock +++ b/bun.lock @@ -6,8 +6,8 @@ "name": "@mosoo/agent-driver", "dependencies": { "@agentclientprotocol/sdk": "1.4.0", - "@anthropic-ai/claude-agent-sdk": "0.3.252", - "@anthropic-ai/sdk": "0.122.0", + "@anthropic-ai/claude-agent-sdk": "0.3.257", + "@anthropic-ai/sdk": "0.123.0", "@modelcontextprotocol/client": "^2.0.0", "@orpc/client": "^1.15.0", "fflate": "^0.8.3", @@ -27,25 +27,25 @@ "packages": { "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.4.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-/eufudw+aFY1LKLolT6yFE6UMmYRl7fMJ/DEONSIyR6wI3slHWITBsANRGqXEY8FRzqUxwh7QEaGiZHcJPVThg=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.252", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.252", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.252", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.252", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.252", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.252", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.252", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.252", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.252" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-hCkyZFn3J46aAMNqS6AZbYz91FaLUmX5VvJOzYZqzlVBJN47OxXQugqOzqa6b6GOZRmwiqW2ck8J8TE7bQZswQ=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.257", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.257", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.257", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.257", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.257", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.257", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.257", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.257", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.257" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-Se55zXv48IYLg/WzoXzpbPLcq86suwDSbRUoNb69l4dkovorqS/47Xuy7MUo/gPNwwcPB4a+aqbXbshU33dcdQ=="], - "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.252", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7lcngnoB3o3ex3lCt54mVhmL4u7ieBqd2g0sLF4YU8GdZap6skJMphe+7p8+upnpmTNJb7QWJYa6+FqINdMknQ=="], + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.257", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ITjFPYB8riu9tbxbrWArokiZ/90w/NDrYbtEvyr3ScilVtu1iupkComxkhvbUxoyT4JRDpKsdw/ZOfwSl/pkpA=="], - "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.252", "", { "os": "darwin", "cpu": "x64" }, "sha512-jznf/im+Ry47vHao6+U8RT2w5gEjxs1ut3EjaoSRVyn35iK40VcjCPjLG7WQMNSqkuxFLTLGxNlKesOw4gXQQg=="], + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.257", "", { "os": "darwin", "cpu": "x64" }, "sha512-0s7QoLRnopbMvCqVpgCnvBWg4UNxPUybMZTknkn3HLBMvUjUdHs1QXb77c/yrT1WAPqDTw1Ju+H0esXwWa8/Kg=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.252", "", { "os": "linux", "cpu": "arm64" }, "sha512-yXet0aNf32MaGJcJvQqMfLv4mfrnBqdKPa1f/KSPQQJA/7v3EFKd2OlBguSwmBJAd0uB671G0PBThzlzcIEiTA=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.257", "", { "os": "linux", "cpu": "arm64" }, "sha512-38tv1s1CaIE6CyEBmJpfKtvwPzrasxPiRT079BEs5aaiLPQ+pmSGF0+Lwy7ot9i6xqr7v5/91TVo5JXHI8Pkpg=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.252", "", { "os": "linux", "cpu": "arm64" }, "sha512-BnFd6MTl2Egid7JuISaJKSmgz8jVm3J/qrnN6u+CKia42k7h5gKawKLi+Jh6FanZQKRTfq/mAQMjiARylaKeMA=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.257", "", { "os": "linux", "cpu": "arm64" }, "sha512-t2WHfKjY4Jjgzfk0lbBUt6EVPQe+wXvXvRmYnK7ICfzZ1jS8CrpDajwBQErKFSMiUqGAutZy3vwDwGziW32cpw=="], - "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.252", "", { "os": "linux", "cpu": "x64" }, "sha512-pSwulGf0yQJu02FzlV/mVoGH2HQMHsBmIhmnKtYF5/zp22vF1vnFANCtUZjyNnvAhmCnkNJRhOe76pzlaRd89w=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.257", "", { "os": "linux", "cpu": "x64" }, "sha512-0FRyIwV4jEJErdDDoYMC0v9lzuFNz5y0lK2340H5fP02eNXsP3U0htKW/bfRP8Ppei+xc4QUZgdCI6rVzkhXGg=="], - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.252", "", { "os": "linux", "cpu": "x64" }, "sha512-SsFTpHDjNCtpCVAlcFp/gwZj8WVsbtORuPZoKujlxNOHjE1uJ9FLXhyo8grFsfxMnyp1QCdsFmFIVt1YO4NDxw=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.257", "", { "os": "linux", "cpu": "x64" }, "sha512-qSlgAUEpj2JAA+YqMD222iv2W3x8xBDSYwdoaOx20VbaJRjFq8feViI7kcr1C6wVN+ApX4Rxl7YX6gekiBkA2g=="], - "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.252", "", { "os": "win32", "cpu": "arm64" }, "sha512-oxtZaKcHiRKI5OIfAogIgw8oNCC6U1E7jqUsl1FvXG72JSMrRFnMViXuE5EVZoJH8m6NkFqzxVuVB7gf8HZZgw=="], + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.257", "", { "os": "win32", "cpu": "arm64" }, "sha512-7iEwy14lSqaAWvNR3KucBusQs5+hG6uoliYWZ2M2FyaCk5PHYymVf9mrRPtWhaYfNwum/YY13acYIIvnAbz0kA=="], - "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.252", "", { "os": "win32", "cpu": "x64" }, "sha512-2Lv3mzxXP1OwY9ZXvKy8Fr1GE8UQS8mKp8lvDexnFfSqVkfs3BfB2qqfuv81H9QCIDX3vu/G+dJKKQxgElrTzA=="], + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.257", "", { "os": "win32", "cpu": "x64" }, "sha512-NW0zMjHXFBdu2TcjT9Zo5o/1tJaDy6A7v4Gt/vLVJgaipV/RzAbyRGXZzH37hswyiKrSLGaSoYYP1vUfncbkUQ=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.122.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-GGPNftt0caaz9MDlmNQGHX8855Ojaduyy5pm9Sm1h7HalCn0cWNb5/bweadJF+4yzbal+QL6ztBa09WAAOzLmQ=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.123.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-Y9oX9mPNGZClHQOFqrWRk43Srcu/UHuPq3rfxxOq7JgW0gi+lJA2MAOK4Ul3k/+AUrwRWFJvd0tK3oC0Pw25dw=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], diff --git a/package.json b/package.json index 61fad37..a553406 100644 --- a/package.json +++ b/package.json @@ -93,8 +93,8 @@ }, "dependencies": { "@agentclientprotocol/sdk": "1.4.0", - "@anthropic-ai/claude-agent-sdk": "0.3.252", - "@anthropic-ai/sdk": "0.122.0", + "@anthropic-ai/claude-agent-sdk": "0.3.257", + "@anthropic-ai/sdk": "0.123.0", "@modelcontextprotocol/client": "^2.0.0", "@orpc/client": "^1.15.0", "fflate": "^0.8.3", diff --git a/src/contract/common.ts b/src/contract/common.ts index e2e4f44..bf4ebbd 100644 --- a/src/contract/common.ts +++ b/src/contract/common.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -export const PROTOCOL_VERSION = 2; +export const PROTOCOL_VERSION = 3; export const protocolVersionSchema = z.literal(PROTOCOL_VERSION); export const timestampSchema = z.iso.datetime({ offset: true }); export const revisionSchema = z.number().int().nonnegative().safe(); diff --git a/src/core/driver-command-dispatcher.ts b/src/core/driver-command-dispatcher.ts index ad2625d..2b5c9db 100644 --- a/src/core/driver-command-dispatcher.ts +++ b/src/core/driver-command-dispatcher.ts @@ -64,6 +64,9 @@ export const ACTIVE_TURN_CANCEL_GRACE_MS = 2_000; // legitimately cross several independent durable-delivery and cleanup epochs. export const ACTIVE_INPUT_SETTLE_GRACE_MS = DRIVER_EVENT_DELIVERY_TIMEOUT_MS * 9; const EXTERNAL_TOOL_EFFECT_FENCE_TIMEOUT_MS = 2_000; +// The remote disposer can spend three independent two-second epochs terminating, +// retrying, and closing before this end-to-end fault-containment ceiling applies. +const MCP_PREPARED_DISPOSE_TIMEOUT_MS = 7_000; export const ACTIVE_MCP_COMMIT_GRACE_MS = AGENT_DRIVER_MCP_EXECUTE_TIMEOUT_MS + 30_000; const MAX_ACTIVE_MCP_COMMANDS = 32; @@ -1121,7 +1124,13 @@ export class DriverCommandDispatcher { ); return this.#resolveExternalToolEffectState(command, settled, effectId); } finally { - await Promise.resolve(prepared[Symbol.asyncDispose]()).catch((error: unknown) => { + await promiseWithTimeout( + Promise.try(() => prepared[Symbol.asyncDispose]()), + { + label: "Prepared MCP command cleanup", + timeoutMs: MCP_PREPARED_DISPOSE_TIMEOUT_MS, + }, + ).catch((error: unknown) => { runtimeContext.logger.warn("driver.runtime.mcp.cleanup.failed", { commandId: command.commandId, message: toErrorMessage(error, "MCP cleanup failed."), diff --git a/src/core/driver-terminal-state.ts b/src/core/driver-terminal-state.ts index 8593402..45c6065 100644 --- a/src/core/driver-terminal-state.ts +++ b/src/core/driver-terminal-state.ts @@ -143,6 +143,13 @@ export class DriverTerminalStateMachine { return this.#activeRun?.runId ?? null; } + rememberOwnedRunId(runId: RunId): void { + if (this.#activeRun !== null && this.#activeRun.runId !== runId) { + throw new Error("Driver handshake run conflicts with the active run."); + } + this.#lastOwnedRunId = runId; + } + selectRunTerminal( ticket: DriverRunTicket, terminal: DriverRunTerminalIdentity, diff --git a/src/infrastructure/runtime/driver-instance-socket.ts b/src/infrastructure/runtime/driver-instance-socket.ts index 5330e64..d7380a7 100644 --- a/src/infrastructure/runtime/driver-instance-socket.ts +++ b/src/infrastructure/runtime/driver-instance-socket.ts @@ -18,6 +18,7 @@ import { } from "../../core/driver-terminal-state"; import type { DriverBootPayload } from "../../protocol/boot"; import type { DriverEventEnvelope, DriverEventInput } from "../../protocol/events"; +import { parseRunId } from "../../protocol/id"; import type { RunId } from "../../protocol/id"; import { driverRuntimeRpcSchemas } from "../../protocol/orpc"; import type { @@ -321,6 +322,9 @@ export class DriverInstanceSocket { throw new Error("Driver socket connection changed during hello."); } + if (result.runId !== null) { + this.#terminalState.rememberOwnedRunId(parseRunId(result.runId)); + } this.#eventBatchMaxSize = result.runConfig.eventBatchMaxSize; return result; } diff --git a/src/runtimes/claude/agent-sdk-driver-backend.ts b/src/runtimes/claude/agent-sdk-driver-backend.ts index da57849..91210ed 100644 --- a/src/runtimes/claude/agent-sdk-driver-backend.ts +++ b/src/runtimes/claude/agent-sdk-driver-backend.ts @@ -1,7 +1,7 @@ import { mkdir } from "node:fs/promises"; import { query, startup } from "@anthropic-ai/claude-agent-sdk"; -import type { Query } from "@anthropic-ai/claude-agent-sdk"; +import type { Query, SDKMessage } from "@anthropic-ai/claude-agent-sdk"; import { DriverTurnCancelledError } from "../../core/driver-runtime-state"; import { @@ -33,6 +33,7 @@ import { ClaudeAgentSdkPrewarm } from "./agent-sdk-prewarm"; import { ClaudeAgentSdkMessageTranslator, ClaudeTerminalWriteError, + type ClaudePreparedResult, type ClaudeTerminalOutcome, } from "./agent-sdk-message-translator"; import { @@ -222,6 +223,7 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { let queryOptionsMs = 0; let runStarted = false; + let preparedResult: ClaudePreparedResult | null = null; let terminalOutcome: ClaudeTerminalOutcome | null = null; try { @@ -318,11 +320,24 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { let firstProviderEventPublished = false; const providerStartedAtMs = Date.now(); for (;;) { - const next = await raceWithAbort(activeQuery.next(), turnSignal); - if (next.done) { + let iteration: IteratorResult; + try { + iteration = await raceWithAbort(activeQuery.next(), turnSignal); + } catch (error) { + if ( + preparedResult !== null && + activeTurn.state === "finalizing" && + !isTurnCancelled(activeTurn) && + activeTurn.abortController.signal.aborted + ) { + break; + } + throw error; + } + if (iteration.done) { break; } - const message = next.value; + const message = iteration.value; if (isTurnCancelled(activeTurn)) { throw new DriverTurnCancelledError("Claude Agent SDK turn was cancelled."); @@ -356,24 +371,43 @@ export class ClaudeAgentSdkDriverBackend implements AgentDriverBackend { throw new DriverTurnCancelledError("Claude Agent SDK turn was cancelled."); } + if (preparedResult !== null) { + if (message.type === "result") { + throw new Error("Claude Agent SDK emitted multiple result frames."); + } + if ( + message.type === "assistant" || + message.type === "stream_event" || + message.type === "user" + ) { + throw new Error("Claude Agent SDK emitted turn content after its result frame."); + } + await this.#messageTranslator.handleSdkMessage(context, message, runId, true); + continue; + } + if (message.type === "result") { activeTurn.state = "finalizing"; - await this.#closeQuery(context, activeTurn, "provider.result"); + preparedResult = await this.#messageTranslator.prepareResult(context, message, runId); + continue; } - terminalOutcome = await this.#messageTranslator.handleSdkMessage(context, message, runId); - if (terminalOutcome !== null) { - break; - } + await this.#messageTranslator.handleSdkMessage(context, message, runId); } - if (terminalOutcome === null && isTurnCancelled(activeTurn)) { + if (preparedResult === null && isTurnCancelled(activeTurn)) { throw new DriverTurnCancelledError("Claude Agent SDK turn was cancelled."); } - if (terminalOutcome === null) { + if (preparedResult === null) { throw new Error("Claude Agent SDK query ended before a result frame."); } + + await this.#closeQuery(context, activeTurn, "provider.result"); + terminalOutcome = await this.#messageTranslator.publishPreparedResult( + context, + preparedResult, + ); } catch (error) { if (!runStarted) { throw error; diff --git a/src/runtimes/claude/agent-sdk-event-writer.ts b/src/runtimes/claude/agent-sdk-event-writer.ts index 00cf56e..fdf9f9f 100644 --- a/src/runtimes/claude/agent-sdk-event-writer.ts +++ b/src/runtimes/claude/agent-sdk-event-writer.ts @@ -86,7 +86,7 @@ export interface ClaudeToolArgumentsEvent { export interface ClaudeToolResultEvent { agentId?: string; authoritative?: boolean; - content: string; + content?: string; context: AgentDriverContext; decisionReason?: string; decisionReasonType?: string; @@ -686,7 +686,7 @@ export class ClaudeAgentSdkEventWriter { kind: "tool.call.updated", payload: { ...(agentId === undefined ? {} : { agentId }), - content, + ...(content === undefined ? {} : { content }), ...(decisionReason === undefined ? {} : { decisionReason }), ...(decisionReasonType === undefined ? {} : { decisionReasonType }), ...(messageId === undefined ? {} : { messageId }), diff --git a/src/runtimes/claude/agent-sdk-message-events.ts b/src/runtimes/claude/agent-sdk-message-events.ts index 31be521..42ed75f 100644 --- a/src/runtimes/claude/agent-sdk-message-events.ts +++ b/src/runtimes/claude/agent-sdk-message-events.ts @@ -63,6 +63,7 @@ function sumModelUsage(modelUsage: unknown, key: string): number | null { export function aggregateClaudeModelUsage(modelUsage: unknown): JsonObject | null { const inputTokens = sumModelUsage(modelUsage, "inputTokens"); const outputTokens = sumModelUsage(modelUsage, "outputTokens"); + const thinkingTokens = sumModelUsage(modelUsage, "thinkingTokens"); const cacheReadTokens = sumModelUsage(modelUsage, "cacheReadInputTokens"); const cacheCreationTokens = sumModelUsage(modelUsage, "cacheCreationInputTokens"); const usage: JsonObject = { @@ -70,6 +71,7 @@ export function aggregateClaudeModelUsage(modelUsage: unknown): JsonObject | nul ...(cacheReadTokens === null ? {} : { cache_read_input_tokens: cacheReadTokens }), ...(inputTokens === null ? {} : { input_tokens: inputTokens }), ...(outputTokens === null ? {} : { output_tokens: outputTokens }), + ...(thinkingTokens === null ? {} : { thinking_tokens: thinkingTokens }), }; return Object.keys(usage).length === 0 ? null : usage; @@ -81,6 +83,7 @@ export function toClaudeUsageUpdatedEvents( ): DriverEventInput[] { const inputTokens = toTokenCount(usage?.["input_tokens"]); const outputTokens = toTokenCount(usage?.["output_tokens"]); + const thoughtTokens = toTokenCount(usage?.["thinking_tokens"]); const cacheReadTokens = toTokenCount(usage?.["cache_read_input_tokens"]); const cacheCreationTokens = toTokenCount(usage?.["cache_creation_input_tokens"]); const totalTokens = sumTokenCounts(inputTokens, outputTokens); @@ -89,6 +92,7 @@ export function toClaudeUsageUpdatedEvents( if ( inputTokens === null && outputTokens === null && + thoughtTokens === null && cacheReadTokens === null && cacheCreationTokens === null && cost === null @@ -108,7 +112,7 @@ export function toClaudeUsageUpdatedEvents( outputTokens, size: null, source: "session_update", - thoughtTokens: null, + thoughtTokens, totalTokens, usageContract: "anthropic_bucketed", used: null, diff --git a/src/runtimes/claude/agent-sdk-message-translator.ts b/src/runtimes/claude/agent-sdk-message-translator.ts index eb5d77f..1b51074 100644 --- a/src/runtimes/claude/agent-sdk-message-translator.ts +++ b/src/runtimes/claude/agent-sdk-message-translator.ts @@ -65,6 +65,12 @@ function exhaustSdkMessage(value: never): unknown { export type { ClaudeTerminalOutcome } from "./agent-sdk-event-writer"; +export interface ClaudePreparedResult { + readonly reason: string; + readonly terminal: ClaudeTerminalOutcome; + readonly toolStatus: "cancelled" | "completed" | "failed"; +} + export class ClaudeTerminalWriteError extends Error { override readonly name = "ClaudeTerminalWriteError"; readonly terminalKind: ClaudeTerminalOutcome["kind"]; @@ -186,7 +192,12 @@ export class ClaudeAgentSdkMessageTranslator { context: AgentDriverContext, message: SDKMessage, runId: RunId, + preserveOpenTurn = false, ): Promise { + if (message.type === "result") { + return this.publishPreparedResult(context, await this.prepareResult(context, message, runId)); + } + const sessionId = readClaudeSdkSessionId(message); if (sessionId !== null && message.type !== "conversation_reset") { @@ -200,17 +211,19 @@ export class ClaudeAgentSdkMessageTranslator { } case "auth_status": case "rate_limit_event": - case "tool_progress": case "tool_use_summary": { await this.#events.pushDiagnostic(context, message); return null; } - case "conversation_reset": { - await this.#handleConversationReset(context, message); + case "tool_progress": { + if (message.heartbeat !== true) { + await this.#events.pushDiagnostic(context, message); + } return null; } - case "result": { - return this.#handleResultMessage(context, message, runId); + case "conversation_reset": { + await this.#handleConversationReset(context, message, preserveOpenTurn); + return null; } case "stream_event": { await this.#handleStreamEvent(context, message, runId); @@ -239,6 +252,30 @@ export class ClaudeAgentSdkMessageTranslator { } } + async prepareResult( + context: AgentDriverContext, + message: Extract, + runId: RunId, + ): Promise { + const sessionId = readClaudeSdkSessionId(message); + if (sessionId !== null) { + await this.#options.recordNativeSessionId(context, sessionId); + } + return this.#prepareResultMessage(context, message, runId); + } + + async publishPreparedResult( + context: AgentDriverContext, + result: ClaudePreparedResult, + ): Promise { + try { + await this.finishTurnWithTerminal(context, result.toolStatus, result.terminal, result.reason); + } catch (error) { + throw new ClaudeTerminalWriteError(error, result.terminal.kind); + } + return result.terminal; + } + async #handleAssistantMessage( context: AgentDriverContext, message: Extract, @@ -683,7 +720,6 @@ export class ClaudeAgentSdkMessageTranslator { case "plugin_install": case "session_state_changed": case "status": - case "task_notification": case "task_progress": case "task_started": case "task_updated": @@ -692,6 +728,10 @@ export class ClaudeAgentSdkMessageTranslator { await this.#events.pushDiagnostic(context, message); return; } + case "task_notification": { + await this.#handleTaskNotification(context, message, runId); + return; + } case "background_tasks_changed": { await this.#handleBackgroundTasksChanged(context, message); return; @@ -718,6 +758,48 @@ export class ClaudeAgentSdkMessageTranslator { ]); } + async #handleTaskNotification( + context: AgentDriverContext, + message: Extract, + runId: RunId, + ): Promise { + await this.#events.pushDiagnostic(context, message); + + if ( + message.status !== "completed" || + message.tool_use_id === undefined || + !message.resource_links?.length + ) { + return; + } + + const structuredOutput = jsonValueSchema.safeParse({ resourceLinks: message.resource_links }); + if (!structuredOutput.success) { + return; + } + + const toolCallId = this.#options.publicToolCallId(message.tool_use_id); + const messageId = + this.#events.toolParentMessageId(toolCallId) ?? + this.#state.activeAssistantMessageId(runId) ?? + this.#state.lastCompletedAssistantMessageId(runId) ?? + null; + await this.#events.ensureToolStarted({ + context, + ...(messageId === null ? {} : { parentMessageId: messageId }), + toolCallId, + toolCallName: "MCP tool", + }); + await this.#events.pushToolResult({ + authoritative: true, + context, + ...(messageId === null ? {} : { messageId }), + status: "completed", + structuredOutput: structuredOutput.data, + toolCallId, + }); + } + async #retractWireItems( context: AgentDriverContext, wireUuids: readonly string[], @@ -768,14 +850,19 @@ export class ClaudeAgentSdkMessageTranslator { async #handleConversationReset( context: AgentDriverContext, message: Extract, + preserveOpenTurn: boolean, ): Promise { - await this.#cancelOpenTurn(context); + if (!preserveOpenTurn) { + await this.#cancelOpenTurn(context); + } await this.#options.replaceNativeSessionId( context, message.session_id, message.new_conversation_id, ); - this.resetTurnMessageState(); + if (!preserveOpenTurn) { + this.resetTurnMessageState(); + } await this.#events.pushSessionInfoUpdated(context, true); } @@ -794,11 +881,11 @@ export class ClaudeAgentSdkMessageTranslator { await this.#options.push(context, "driver.claude.files.persisted", events); } - async #handleResultMessage( + async #prepareResultMessage( context: AgentDriverContext, message: Extract, runId: RunId, - ): Promise { + ): Promise { const successful = isClaudeResultSuccessful(message); const cancelled = isClaudeResultCancelled(message); @@ -841,21 +928,14 @@ export class ClaudeAgentSdkMessageTranslator { aggregateClaudeModelUsage(message.modelUsage), message.total_cost_usd, ); - const finishResult = async ( + const prepareResult = ( toolStatus: "cancelled" | "completed" | "failed", terminal: ClaudeTerminalOutcome, reason: string, - ): Promise => { - try { - await this.finishTurnWithTerminal(context, toolStatus, terminal, reason); - } catch (error) { - throw new ClaudeTerminalWriteError(error, terminal.kind); - } - return terminal; - }; + ): ClaudePreparedResult => ({ reason, terminal, toolStatus }); if (cancelled) { - return finishResult( + return prepareResult( "cancelled", this.#events.runCancelled(runId, message.terminal_reason ?? "provider.aborted"), "driver.claude.turn.cancelled", @@ -870,7 +950,7 @@ export class ClaudeAgentSdkMessageTranslator { : jsonValueSchema.safeParse(message.structured_output); if (structuredOutput !== undefined && !structuredOutput.success) { - return finishResult( + return prepareResult( "failed", this.#events.runError( runId, @@ -889,7 +969,7 @@ export class ClaudeAgentSdkMessageTranslator { MAX_CLAUDE_STRUCTURED_TERMINAL_BYTES; if (structuredOutputTooLarge) { - return finishResult( + return prepareResult( "failed", this.#events.runError( runId, @@ -922,14 +1002,14 @@ export class ClaudeAgentSdkMessageTranslator { ? null : this.#state.resolveFinalAssistantSnapshot(runId, resultText); - return finishResult( + return prepareResult( "completed", this.#events.runFinished(runId, finalMessage, structuredOutput?.data), "driver.claude.turn.completed", ); } - return finishResult( + return prepareResult( "failed", this.#events.runError( runId, diff --git a/tests/claude-agent-sdk-driver-backend.test.ts b/tests/claude-agent-sdk-driver-backend.test.ts index f925555..7b51fc0 100644 --- a/tests/claude-agent-sdk-driver-backend.test.ts +++ b/tests/claude-agent-sdk-driver-backend.test.ts @@ -1889,8 +1889,9 @@ describe("Claude Agent SDK driver backend", () => { expect(harness.events.filter(({ kind }) => kind === "run.completed")).toHaveLength(2); }); - test("requires one result frame and ignores provider frames after it", async () => { + test("requires one result frame and drains informational frames before its terminal", async () => { delete process.env[PREWARM_ENV]; + let closedAfterTail = false; let queryIndex = 0; let lateFrameRead = false; const harness = createHarness({ @@ -1907,13 +1908,27 @@ describe("Claude Agent SDK driver backend", () => { yield resultMessage(); lateFrameRead = true; yield { - message: { content: [{ text: "late", type: "text" }] }, - parent_tool_use_id: null, + output_file: "/tmp/report.pdf", + resource_links: [ + { + mimeType: "application/pdf", + name: "report.pdf", + uri: "file:///workspace/report.pdf", + }, + ], session_id: "native-session-1", - type: "assistant", - uuid: "late-assistant", + status: "completed", + subtype: "task_notification", + summary: "done", + task_id: "task-resource", + tool_use_id: "tool-resource", + type: "system", + uuid: "post-result-resource", } as unknown as SDKMessage; })(), + () => { + closedAfterTail = lateFrameRead; + }, ); }, startup: async () => { @@ -1933,19 +1948,138 @@ describe("Claude Agent SDK driver backend", () => { ); await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); - expect(lateFrameRead).toBe(false); + const resourceEventIndex = harness.events.findIndex( + (event) => + event.kind === "tool.call.updated" && + isRecord(event.payload) && + isRecord(event.payload["structuredOutput"]), + ); + const terminalEventIndex = harness.events.findIndex( + (event) => event.kind === "run.completed" && event.runId === DRIVER_TEST_IDS.secondRunId, + ); + expect(lateFrameRead).toBe(true); + expect(closedAfterTail).toBe(true); + expect(resourceEventIndex).toBeGreaterThanOrEqual(0); + expect(harness.events[resourceEventIndex]).toMatchObject({ + kind: "tool.call.updated", + payload: { + status: "completed", + structuredOutput: { + resourceLinks: [ + { + mimeType: "application/pdf", + name: "report.pdf", + uri: "file:///workspace/report.pdf", + }, + ], + }, + toolCallId: "tool-resource", + }, + }); + expect(terminalEventIndex).toBeGreaterThan(resourceEventIndex); + }); + + test("fails closed on turn content after a result frame", async () => { + delete process.env[PREWARM_ENV]; + const harness = createHarness({ + createQueryOptions: async () => ({}), + query: () => + fakeQuery([ + resultMessage(), + { + message: { content: [{ text: "late", type: "text" }] }, + parent_tool_use_id: null, + session_id: "native-session-1", + type: "assistant", + uuid: "late-assistant", + } as unknown as SDKMessage, + ]), + startup: async () => { + throw new Error("prewarm is disabled"); + }, + }); + + await expect( + harness.backend.handleInput(harness.context, { text: "terminal" }, DRIVER_TEST_IDS.runId), + ).rejects.toThrow("turn content after its result frame"); + await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); + + expect(harness.events.some((event) => event.kind === "run.completed")).toBe(false); + expect(harness.events.filter((event) => event.kind === "run.failed")).toHaveLength(1); expect( harness.events.some( (event) => event.kind === "message.delta" && - typeof event.payload === "object" && - event.payload !== null && - "contentDelta" in event.payload && - event.payload.contentDelta === "late", + isRecord(event.payload) && + event.payload["contentDelta"] === "late", ), ).toBe(false); }); + test("keeps result bookkeeping causal across a post-result conversation reset", async () => { + delete process.env[PREWARM_ENV]; + const harness = createHarness({ + createQueryOptions: async () => ({}), + query: () => + fakeQuery([ + { + message: { + content: [ + { + id: "tool-before-reset", + input: {}, + name: "Bash", + type: "tool_use", + }, + ], + }, + parent_tool_use_id: null, + session_id: "native-session-1", + type: "assistant", + uuid: "assistant-before-reset", + }, + resultMessage("native-session-1"), + { + new_conversation_id: "native-session-2", + session_id: "native-session-1", + type: "conversation_reset", + uuid: "post-result-reset", + }, + ] as unknown as SDKMessage[]), + startup: async () => { + throw new Error("prewarm is disabled"); + }, + }); + + await harness.backend.handleInput(harness.context, { text: "terminal" }, DRIVER_TEST_IDS.runId); + await harness.backend.stop(harness.context, "test.complete", new AbortController().signal); + + const resetIndex = harness.events.findIndex( + (event) => + event.kind === "runtime.resume.updated" && + isRecord(event.payload) && + event.payload["resumePointer"] === "native-session-2", + ); + const terminalIndex = harness.events.findIndex((event) => event.kind === "run.completed"); + expect(resetIndex).toBeGreaterThanOrEqual(0); + expect(terminalIndex).toBeGreaterThan(resetIndex); + expect(harness.events.filter((event) => event.kind === "run.completed")).toHaveLength(1); + expect(harness.events.some((event) => event.kind === "run.failed")).toBe(false); + expect( + harness.events.flatMap((event) => { + if ( + event.kind !== "tool.call.updated" || + !isRecord(event.payload) || + event.payload["toolCallId"] !== "tool-before-reset" || + !["cancelled", "completed", "failed"].includes(String(event.payload["status"])) + ) { + return []; + } + return [event.payload["status"]]; + }), + ).toEqual(["completed"]); + }); + function recoveryPayload( recoveryMessages: DriverStartInput["execution"]["session"]["recoveryMessages"], nativeResumeRef: DriverStartInput["execution"]["session"]["nativeResumeRef"] = null, diff --git a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts index 47e4ccc..1a33655 100644 --- a/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts +++ b/tests/claude-agent-sdk-provider-fixtures-transcript.test.ts @@ -45,8 +45,25 @@ describe("Claude Agent SDK provider fixtures", () => { type: "system", uuid: "task-updated-1", }, + { + elapsed_time_seconds: 5, + heartbeat: true, + parent_tool_use_id: null, + session_id: "native-session-1", + tool_name: "Agent", + tool_use_id: "tool-1", + type: "tool_progress", + uuid: "tool-heartbeat-1", + }, { output_file: "/tmp/private-task-output", + resource_links: [ + { + mimeType: "application/pdf", + name: "report.pdf", + uri: "file:///workspace/report.pdf", + }, + ], session_id: "native-session-1", status: "completed", subtype: "task_notification", @@ -110,6 +127,24 @@ describe("Claude Agent SDK provider fixtures", () => { }); expect(JSON.stringify(taskEvents)).not.toContain("private"); expect(events().filter((event) => event.kind === "diagnostic.reported")).toHaveLength(4); + expect(events()).toContainEqual( + expect.objectContaining({ + kind: "tool.call.updated", + payload: expect.objectContaining({ + status: "completed", + structuredOutput: { + resourceLinks: [ + { + mimeType: "application/pdf", + name: "report.pdf", + uri: "file:///workspace/report.pdf", + }, + ], + }, + toolCallId: "tool-1", + }), + }), + ); }); test("closes visible tasks before the run terminal and resets them between turns", async () => { @@ -469,6 +504,13 @@ describe("Claude Agent SDK provider fixtures", () => { }, ], tool_use_result: { + resourceLinks: [ + { + mimeType: "application/pdf", + name: "report.pdf", + uri: "file:///workspace/report.pdf", + }, + ], usage: { output_tokens_details: { thinking_tokens: 3 } }, }, type: "user", @@ -485,6 +527,13 @@ describe("Claude Agent SDK provider fixtures", () => { nonExecutionKind: "interrupted", status: "cancelled", structuredOutput: { + resourceLinks: [ + { + mimeType: "application/pdf", + name: "report.pdf", + uri: "file:///workspace/report.pdf", + }, + ], usage: { output_tokens_details: { thinking_tokens: 3 } }, }, toolCallId: "tool-1", diff --git a/tests/contract-executor-mutation.test.ts b/tests/contract-executor-mutation.test.ts index e5296fa..cd262dd 100644 --- a/tests/contract-executor-mutation.test.ts +++ b/tests/contract-executor-mutation.test.ts @@ -46,7 +46,7 @@ function session(id = createDriverId()): Session { function snapshot(sessionValue = session()): SessionSnapshot { return validateSessionSnapshot({ - protocolVersion: 2, + protocolVersion: 3, revision: 0, capturedAt: time, session: sessionValue, @@ -754,7 +754,7 @@ describe("contract Preview lane", () => { describe("contract closed core", () => { test("keeps Coordinator resource policies out of peer negotiation", () => { const initialize = { - protocolVersion: 2, + protocolVersion: 3, role: "observer", implementation: { name: "observer", version: "1" }, capabilities: {}, @@ -780,7 +780,7 @@ describe("contract closed core", () => { expect( initializeResultSchema.safeParse({ - protocolVersion: 2, + protocolVersion: 3, implementation: { name: "coordinator", version: "1" }, capabilities: {}, limits, diff --git a/tests/contract-invariant.test.ts b/tests/contract-invariant.test.ts index a4d47ac..cda2c38 100644 --- a/tests/contract-invariant.test.ts +++ b/tests/contract-invariant.test.ts @@ -46,7 +46,7 @@ function session(id = createDriverId()): Session { function snapshot(sessionValue = session()): SessionSnapshot { return validateSessionSnapshot({ - protocolVersion: 2, + protocolVersion: 3, revision: 0, capturedAt: time, session: sessionValue, diff --git a/tests/contract-preview.test.ts b/tests/contract-preview.test.ts index cf1b859..20392ee 100644 --- a/tests/contract-preview.test.ts +++ b/tests/contract-preview.test.ts @@ -47,7 +47,7 @@ function session(id = createDriverId()): Session { function snapshot(sessionValue = session()): SessionSnapshot { return validateSessionSnapshot({ - protocolVersion: 2, + protocolVersion: 3, revision: 0, capturedAt: time, session: sessionValue, @@ -178,7 +178,7 @@ describe("contract closed core", () => { maxSnapshotBytes: 1, }); const result = { - protocolVersion: 2, + protocolVersion: 3, implementation: { name: "coordinator", version: "1" }, capabilities: {}, limits, @@ -192,7 +192,7 @@ describe("contract closed core", () => { test("reserves the complete JSON-RPC envelope in frame limits", () => { const result = { - protocolVersion: 2, + protocolVersion: 3, implementation: { name: "coordinator", version: "1" }, capabilities: {}, limits: protocolLimits({ maxCommandBytes: 4_095, maxFrameBytes: 4_096 }), @@ -225,7 +225,7 @@ describe("contract closed core", () => { capabilities: {}, implementation: { name: "coordinator", version: "1" }, limits: protocolLimits({ maxFrameBytes, maxSnapshotBytes }), - protocolVersion: 2, + protocolVersion: 3, }); expect(initializeResultSchema.safeParse(result(maxFrameBytes - envelopeBytes)).success).toBe( diff --git a/tests/contract-reducer.test.ts b/tests/contract-reducer.test.ts index ded53f4..1595483 100644 --- a/tests/contract-reducer.test.ts +++ b/tests/contract-reducer.test.ts @@ -41,7 +41,7 @@ function session(id = createDriverId()): Session { function snapshot(sessionValue = session()): SessionSnapshot { return validateSessionSnapshot({ - protocolVersion: 2, + protocolVersion: 3, revision: 0, capturedAt: time, session: sessionValue, @@ -619,7 +619,7 @@ describe("contract authority reducer", () => { }; const nextRun = activeRun(); const value = validateSessionSnapshot({ - protocolVersion: 2, + protocolVersion: 3, revision: 2, capturedAt: time, session: sessionValue, @@ -648,7 +648,7 @@ describe("contract authority reducer", () => { endedAt: time, }; const current = validateSessionSnapshot({ - protocolVersion: 2, + protocolVersion: 3, revision: 1, capturedAt: time, session: sessionValue, diff --git a/tests/contract-wire.test.ts b/tests/contract-wire.test.ts index 22a0604..61981a7 100644 --- a/tests/contract-wire.test.ts +++ b/tests/contract-wire.test.ts @@ -41,7 +41,7 @@ function session(id = createDriverId()): Session { function snapshot(sessionValue = session()): SessionSnapshot { return validateSessionSnapshot({ - protocolVersion: 2, + protocolVersion: 3, revision: 0, capturedAt: time, session: sessionValue, diff --git a/tests/driver-external-tool-effect-v3.test.ts b/tests/driver-external-tool-effect-v3.test.ts index a3bceac..b4e6a69 100644 --- a/tests/driver-external-tool-effect-v3.test.ts +++ b/tests/driver-external-tool-effect-v3.test.ts @@ -644,6 +644,7 @@ describe("durable external MCP effect protocol v3", () => { test("rechecks exact run ownership after preparation and before claim", async () => { const input = command("run-changed-before-claim"); + const shutdown = new AbortController(); let currentRunId = DRIVER_TEST_IDS.runId; let claims = 0; let executions = 0; @@ -659,7 +660,7 @@ describe("durable external MCP effect protocol v3", () => { }; const runtime = createDispatcher({ backend: createBackend(), - isShuttingDown: () => socket.isDrained(), + isShuttingDown: () => shutdown.signal.aborted, mcpPrepare: async () => { currentRunId = DRIVER_TEST_IDS.secondRunId; return { @@ -671,10 +672,13 @@ describe("durable external MCP effect protocol v3", () => { }; }, runtimeState: new DriverRuntimeStateMachine("ready"), + shutdownSignal: shutdown.signal, }); - await runtime.dispatcher.run(socket, runtime.logger); + const run = runtime.dispatcher.run(socket, runtime.logger); await waitForUpdate(socket, (update) => update.status === "failed"); + shutdown.abort(new Error("test complete")); + await run; expect(claims).toBe(0); expect(executions).toBe(0); @@ -1170,4 +1174,72 @@ describe("durable external MCP effect protocol v3", () => { }); expect(runtime.shutdownCalls).toEqual([]); }); + + test("waits for MCP cleanup beyond one internal cleanup epoch", async () => { + const input = command("cleanup-multiple-epochs"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + let disposed = false; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => ({ + async execute() { + return { + outputText: "durable result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + }, + async [Symbol.asyncDispose]() { + await Bun.sleep(2_250); + disposed = true; + }, + }), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + + expect(disposed).toBe(true); + expect(socket.updates.at(-1)).toMatchObject({ + commandId: input.commandId, + status: "completed", + }); + }); + + test("bounds a permanently stalled MCP cleanup", async () => { + const input = command("cleanup-stalled"); + const socket = new FakeDriverRuntimeIo([input], DRIVER_TEST_IDS.runId); + let disposeCalls = 0; + const runtime = createDispatcher({ + backend: createBackend(), + isShuttingDown: () => socket.isDrained(), + mcpPrepare: async () => ({ + async execute() { + return { + outputText: "durable result", + requestId: input.requestId, + serverId: input.serverId, + toolName: input.toolName, + }; + }, + async [Symbol.asyncDispose]() { + disposeCalls += 1; + await new Promise(() => {}); + }, + }), + runtimeState: new DriverRuntimeStateMachine("ready"), + }); + + await runtime.dispatcher.run(socket, runtime.logger); + await waitForUpdate(socket, (update) => update.status === "completed"); + + expect(disposeCalls).toBe(1); + expect(socket.updates.at(-1)).toMatchObject({ + commandId: input.commandId, + status: "completed", + }); + expect(runtime.shutdownCalls).toEqual([]); + }, 10_000); }); diff --git a/tests/driver-instance-socket-process.test.ts b/tests/driver-instance-socket-process.test.ts index e87aca1..836dd7f 100644 --- a/tests/driver-instance-socket-process.test.ts +++ b/tests/driver-instance-socket-process.test.ts @@ -4,6 +4,7 @@ import { DriverProcess } from "../src/bin/driver-process"; import { DriverInstanceSocket } from "../src/infrastructure/runtime/driver-instance-socket"; import type { AgentDriverContext } from "../src/core/agent-driver-backend"; import { DriverTurnCancelledError } from "../src/core/driver-runtime-state"; +import type { DriverBootPayload } from "../src/protocol/boot"; import type { RuntimeCommand } from "../src/runtime-command"; import { settlePromiseWithTimeout } from "../src/utils/async"; import { DRIVER_TEST_IDS, driverBootPayload } from "./driver-boot-payload-fixture"; @@ -59,6 +60,7 @@ class RpcWebSocket extends OpenWebSocket { static eventBatchMaxSize = 2; static heartbeatFails = false; static heartbeatIntervalMs = 1_000; + static helloRunId: string | null = null; static lostResponsePath: string | null = null; static pathObserver: ((path: string) => void) | null = null; static receiptOverride: Partial<{ eventId: string; seq: number; type: string }> | null = null; @@ -135,7 +137,7 @@ class RpcWebSocket extends OpenWebSocket { eventBatchMaxSize: RpcWebSocket.eventBatchMaxSize, organizationPath: "/workspace", }, - runId: null, + runId: RpcWebSocket.helloRunId, }; } else if (path === "/driver/pushEvents") { const events = ( @@ -206,6 +208,7 @@ afterEach(() => { RpcWebSocket.eventBatchMaxSize = 2; RpcWebSocket.heartbeatFails = false; RpcWebSocket.heartbeatIntervalMs = 1_000; + RpcWebSocket.helloRunId = null; RpcWebSocket.lostResponsePath = null; RpcWebSocket.pathObserver = null; RpcWebSocket.receiptOverride = null; @@ -451,6 +454,32 @@ describe("DriverProcess lifecycle", () => { expect(paths.filter((path) => path === "/driver/failRun")).toHaveLength(2); }); + test("reports startup failure to the run adopted during hello", async () => { + globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; + RpcWebSocket.helloRunId = DRIVER_TEST_IDS.secondRunId; + const prewarmPayload = { + ...driverBootPayload, + execution: { + ...driverBootPayload.execution, + configRevision: { + ...driverBootPayload.execution.configRevision, + runId: null, + }, + }, + } satisfies DriverBootPayload; + + await expect( + new DriverProcess(prewarmPayload, () => { + throw new Error("backend factory failed"); + }).run(), + ).rejects.toThrow("backend factory failed"); + + const failure = (PendingWebSocket.instances[0] as RpcWebSocket).requests.find( + ({ path }) => path === "/driver/failRun", + ); + expect(failure?.input).toMatchObject({ runId: DRIVER_TEST_IDS.secondRunId }); + }); + test("fails the exact second run when its provider exits before a run event", async () => { globalThis.WebSocket = RpcWebSocket as unknown as typeof WebSocket; RpcWebSocket.commands = [ diff --git a/tests/driver-terminal-state.test.ts b/tests/driver-terminal-state.test.ts index e7282dc..9cb0bad 100644 --- a/tests/driver-terminal-state.test.ts +++ b/tests/driver-terminal-state.test.ts @@ -222,4 +222,15 @@ describe("DriverTerminalStateMachine", () => { expect(state.terminalRunId(DRIVER_TEST_IDS.runId)).toBe(DRIVER_TEST_IDS.secondRunId); }); + + test("remembers a handshake run without activating it", () => { + const state = new DriverTerminalStateMachine(); + + state.rememberOwnedRunId(DRIVER_TEST_IDS.secondRunId); + + expect(state.currentRunId()).toBeNull(); + expect(state.terminalRunId(DRIVER_TEST_IDS.runId)).toBe(DRIVER_TEST_IDS.secondRunId); + state.beginRun(DRIVER_TEST_IDS.secondRunId); + expect(() => state.rememberOwnedRunId(DRIVER_TEST_IDS.runId)).toThrow("active run"); + }); }); diff --git a/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json b/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json index cbd7a23..aaa7e00 100644 --- a/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json +++ b/tests/fixtures/providers/claude-agent-sdk/cases/stream-text-thinking-tool-result.json @@ -88,6 +88,7 @@ "inputTokens": 3, "maxOutputTokens": 64000, "outputTokens": 5, + "thinkingTokens": 2, "webSearchRequests": 0 } }, @@ -203,7 +204,7 @@ "outputTokens": 5, "size": null, "source": "session_update", - "thoughtTokens": null, + "thoughtTokens": 2, "totalTokens": 8, "usageContract": "anthropic_bucketed", "used": null diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index 1fc559e..250b9e9 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -38,8 +38,9 @@ describe("public API", () => { const heartbeatReason = "ping" satisfies orpc.DriverHeartbeatInput["reason"]; expect(boot.DRIVER_PROTOCOL_VERSION).toBe(3); - expect(contract.PROTOCOL_VERSION).toBe(2); - expect(contract.protocolVersionSchema.parse(2)).toBe(2); + expect(contract.PROTOCOL_VERSION).toBe(3); + expect(contract.protocolVersionSchema.parse(3)).toBe(3); + expect(contract.protocolVersionSchema.safeParse(2).success).toBe(false); expect(contract.sessionSnapshotSchema.parse).toBeFunction(); expect(cmaHttp.createCmaHttpHandler).toBe(agentDriver.createCmaHttpHandler); expect(cmaSdk.CmaSdkClient).toBe(agentDriver.CmaSdkClient);